QUESTION 1: Is there a better way to test that callbacks are being called?
it "must trigger an event when the danger zone is reached" do
alert = false
game.on_event(:enter_region, :danger_zone) { alert = true }
game.on_event(:leave_region, :danger_zone) { alert = false }
refute alert, "should not enable alert before danger zone is reached"
game.move(100,0)
assert alert, "should endable alert once danger zone is reached"
game.move(-1, -1)
refute alert, "should disable alert once danger zone is exited"
end
QUESTION 2: Is there a better way to test symmetric numerical computations (such as the borders of a rectangle)?
it "must be able to detect minimum distance from boundaries" do
map = new_map
player = new_element("player 1")
# starting at (10,25)
map.place(player, 10, 25)
map.nearest_boundary(player).must_equal(5)
# now at (10, 7)
map.move(player, 0, -18)
map.nearest_boundary(player).must_equal(2)
# now at (10, 91)
map.move(player, 0, 84)
map.nearest_boundary(player).must_equal(4)
# now at (92, 91)
map.move(player, 82, 0)
map.nearest_boundary(player).must_equal(3)
end
QUESTION 3: I was happy with the following acceptance tests, but the support code they rely on is extremely brittle. Should I instead have thought about testing the presenter and not bothering with the full outside-in tests? I could have probably cornered most or all of the bugs I ran into with the untested frontend code that way.
it "should result in a loss on mine collision" do
world = Blind::Worlds.original(1)
levels = [Blind::Level.new(world, "test")]
game = Blind::UI::GamePresenter.new(levels)
sim = Blind::UI::Simulator.new(game)
mine = world.positions.first(:mine)
sim.move(mine.x, mine.y)
sim.status.must_equal "You got blasted by a mine! YOU LOSE!"
end
QUESTION 4: I occasionally have test failures due to floating point errors in distance computations. I would just check that they were within some delta, but the problem is that right now the borders between boundaries in Blind are zero-width. This means the difference between 19.9999999999999999999997 and 20.0 can put something in one region or the other, and the difference between 7.99997 and 8 could cause a mine to explode or not. The former problem can be solved through rejection sampling, the latter is something that does not affect game play because the level of precision does not matter so much. However, because these problems tend to cascade upwards through the tests, I really struggled with them. Any thoughts? (I cheated in some places and just serialized objects that I knew didn't have unexpected rounding issues, but that feels WRONG)
QUESTION 5: How do you test randomization, or do you consider it an implementation detail? I.e. what tests would you write for this feature?
def self.random(distance_range)
angle = rand(0..2*Math::PI)
length = rand(distance_range)
x = length*Math.cos(angle)
y = length*Math.sin(angle)
point = new(x, y)
center = new(0, 0)
if distance_range.include?(point.distance(center))
point
else
random(distance_range)
end
end
QUESTION 6: Related to the previous question, how do you test code which relies on randomization? Do you generate cached fixtures with predictable results? Write tests that are randomness aware? Make artificial test cases? For example, if you want to test that when a player reaches the exit position, the game ends in a win, how do you make sure that the player doesn't crash into a mine along the way? I ended up creating an artificial scenario with no mines to ensure that the results were deterministic, but it seems to be a cheat. (Similarly, I made a map with one mine to test mine collisions at a specific position)
QUESTION 7: Do you have recommendations for tightening the feedback loop when making major refactors? This is a disaster.
QUESTION 8: Would you write something different here? The use of magic numbers is somewhat innocuous in this case, but makes it so you have to think through the computation in order to understand the test. Does it just need better documentation?
it "must compute distance between itself and another point" do
point_a = Blind::Point.new(2, 3)
point_b = Blind::Point.new(-1, 7)
point_a.distance(point_b).must_equal(5)
point_b.distance(point_a).must_equal(5)
end
QUESTION 9: The magic numbers in this file are a lot worse, because they refer to values buried in a configuration file. From a high level perspective, how would you do this differently?