Python Asteroids on GitHub

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:
ship = ships[0]
degrees = self.angle_to(ship.position)
else:
degrees = random.random() * 360.0
``````

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):

def missile_spec(self, ships):
if ships and random.random() <= u.SAUCER_TARGETING_FRACTION:
ship = ships[0]
degrees = self.angle_to(ship.position)
else:
degrees = random.random() * 360.0
``````

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:
ship = ships[0]
degrees = self.angle_to(ship.position)
else:
random_angle = random.random()
degrees = random_angle * 360.0
``````

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:
ship = ships[0]
degrees = self.angle_to(ship.position)
else:
degrees = random_angle * 360.0
``````

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)

def missile_spec(self, random_angle, should_target, ships):
if ships and should_target <= u.SAUCER_TARGETING_FRACTION:
ship = ships[0]
degrees = self.angle_to(ship.position)
else:
degrees = random_angle * 360.0
``````

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)

def missile_spec(self, should_target, random_angle, ships):
if ships and should_target <= u.SAUCER_TARGETING_FRACTION:
ship = ships[0]
degrees = self.angle_to(ship.position)
else:
degrees = random_angle * 360.0
``````

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 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 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)
So, now the `missile_spec` “branching logic” is tested. A good thing. Commit: refactor create_missile for testability, and test resulting missile_spec method.