Python 049 - Test Improvement
I don’t know why I needed a hint for this test, but I’m glad to have had it.
I commented last time that I felt the testing was a bit light, citing this method as one that I didn’t see how to test:
def create_missile(self, ships):
if ships and random.random() <= u.SAUCER_TARGETING_FRACTION:
velocity_adjustment = Vector2(0, 0)
ship = ships[0]
degrees = self.angle_to(ship.position)
else:
velocity_adjustment = self.velocity
degrees = random.random() * 360.0
return self.missile_at_angle(degrees, velocity_adjustment)
GeePaw Hill wrote:
That if is a function returning missile-spec, composed of degrees and velocity. The consequent is a testable method. The alternate, if passed a float, is too. Extract the whole if to take the two floats, call it missle_spec(ships,float,float). It’s testable. Now the body of create_missle() is an untested one-liner, return self.missle_at_angle(self.missle_spec(ships, random.random(), random.random()). (edited)
Let’s try it just as he spoke it. The missile_spec
will need to be some kind of structish thing, or just a sequence, since it’s local to what we’re doing here.
PyCharm does the extract like this:
def create_missile(self, ships):
degrees, velocity_adjustment = self.missile_spec(ships)
return self.missile_at_angle(degrees, velocity_adjustment)
def missile_spec(self, ships):
if ships and random.random() <= u.SAUCER_TARGETING_FRACTION:
velocity_adjustment = Vector2(0, 0)
ship = ships[0]
degrees = self.angle_to(ship.position)
else:
velocity_adjustment = self.velocity
degrees = random.random() * 360.0
return degrees, velocity_adjustment
I could proceed from there but let’s do better. I’ll extract two random variables:
def create_missile(self, ships):
should_target = random.random()
if ships and should_target <= u.SAUCER_TARGETING_FRACTION:
velocity_adjustment = Vector2(0, 0)
ship = ships[0]
degrees = self.angle_to(ship.position)
else:
velocity_adjustment = self.velocity
random_angle = random.random()
degrees = random_angle * 360.0
return self.missile_at_angle(degrees, velocity_adjustment)
Now I’ll move the random_angle
up to the top:
def create_missile(self, ships):
should_target = random.random()
random_angle = random.random()
if ships and should_target <= u.SAUCER_TARGETING_FRACTION:
velocity_adjustment = Vector2(0, 0)
ship = ships[0]
degrees = self.angle_to(ship.position)
else:
velocity_adjustment = self.velocity
degrees = random_angle * 360.0
return self.missile_at_angle(degrees, velocity_adjustment)
Now extract the if
:
def create_missile(self, ships):
should_target = random.random()
random_angle = random.random()
degrees, velocity_adjustment = self.missile_spec(random_angle, should_target, ships)
return self.missile_at_angle(degrees, velocity_adjustment)
def missile_spec(self, random_angle, should_target, ships):
if ships and should_target <= u.SAUCER_TARGETING_FRACTION:
velocity_adjustment = Vector2(0, 0)
ship = ships[0]
degrees = self.angle_to(ship.position)
else:
velocity_adjustment = self.velocity
degrees = random_angle * 360.0
return degrees, velocity_adjustment
I like that PyCharm is perfectly happy returning two things from the function. So am I.
I wish I had reversed the random arguments so I do that:
def create_missile(self, ships):
should_target = random.random()
random_angle = random.random()
degrees, velocity_adjustment = self.missile_spec(should_target, random_angle, ships)
return self.missile_at_angle(degrees, velocity_adjustment)
def missile_spec(self, should_target, random_angle, ships):
if ships and should_target <= u.SAUCER_TARGETING_FRACTION:
velocity_adjustment = Vector2(0, 0)
ship = ships[0]
degrees = self.angle_to(ship.position)
else:
velocity_adjustment = self.velocity
degrees = random_angle * 360.0
return degrees, velocity_adjustment
OK, now, as Hill predicts, we can test missile_spec
and create_missile
doesn’t need it. So to test:
def test_missile_spec_targeted(self):
saucer = Saucer(Vector2(100, 110))
saucer.velocity = Vector2(99, 77)
ships = [Ship(Vector2(100, 100))]
should_target = 0.1
random_angle = None
degrees, velocity_adjustment = saucer.missile_spec(should_target, random_angle, ships)
assert velocity_adjustment == Vector2(0, 0)
assert degrees == -90
def test_missile_spec_no_ship(self):
saucer = Saucer(Vector2(100, 110))
saucer.velocity = Vector2(99, 77)
ships = []
should_target = 0.1
random_angle = 0.5
degrees, velocity_adjustment = saucer.missile_spec(should_target, random_angle, ships)
assert velocity_adjustment == saucer.velocity
assert degrees == 180
def test_missile_spec_no_dice(self):
saucer = Saucer(Vector2(100, 110))
saucer.velocity = Vector2(99, 77)
ships = [Ship(Vector2(100, 100))]
should_target = 0.26
random_angle = 0.5
degrees, velocity_adjustment = saucer.missile_spec(should_target, random_angle, ships)
assert velocity_adjustment == saucer.velocity
assert degrees == 180
I think those cover us pretty well.
So, now the missile_spec
“branching logic” is tested. A good thing. Commit: refactor create_missile for testability, and test resulting missile_spec method.
A good afternoon’s work. Thanks, Hill!
See you next time!