Can't Resist
It almost seems like I can’t work on the code unless I’m typing an article. I guess I just don’t want you to miss anything interesting that might happen. No promises, mind you.
Yesterday, I found another batch of tests that are all very similar and are too “round-trippy”:
def test_bot_cannot_drop_off_world_north(self):
world = World(10, 10)
bot: Bot = DirectConnection(world).add_bot(5, 0)
block = Block(4, 4)
bot.receive(block)
bot.direction = Direction.NORTH
world.drop_forward(bot, block)
assert bot.has(block), 'drop should not happen'
def test_bot_cannot_drop_off_world_east(self):
...
def test_bot_cannot_drop_off_world_south(self):
...
def test_bot_cannot_drop_off_world_west(self):
...
First, I’ll recast the first one to be simpler. We already have confidence from other tests that the general flow into DirectConnection and back works. We just want to know that the World will not allow a drop if it would drop the block off-world.
There is an irritating issue. We’d really like to start this test with the bot holding a block. But the only way we currently have to make that happen is to create a block in the world, position the bot properly, and do a take, which removes the block from the world and gives it to the bot.
Here’s the relevant code:
class World:
def add_block(self, x, y, aroma=0):
entity = WorldEntity.block(x, y, aroma)
self.map.place(entity)
return entity.id
def take_forward(self, bot):
is_block = lambda e: e.name == 'B'
if block := self.map.take_conditionally_at(bot.forward_location(), is_block):
bot.receive(block)
class WorldEntity:
def receive(self, entity):
self.holding = entity
I think that I’d like to have the ability to create the entity without placing it. Let’s do it directly first, then see if we want to push some behavior over to World.
def test_bot_cannot_drop_off_world_north():
world = World(10, 10)
bot_id = world.add_bot(5, 0, Direction.NORTH)
bot = world.entity_from_id(bot_id)
block = WorldEntity.block(4, 4)
bot.receive(block)
world.drop_forward(bot, block)
assert bot.has(block), 'drop should not happen'
That’s not too awful. The only thing that changes in the tests is the starting position and direction of the bot. I’ll extract variables:
def test_bot_cannot_drop_off_world_north():
x = 5
y = 0
direction = Direction.NORTH
world = World(10, 10)
bot_id = world.add_bot(x, y, direction)
bot = world.entity_from_id(bot_id)
block = WorldEntity.block(4, 4)
bot.receive(block)
world.drop_forward(bot, block)
assert bot.has(block), 'drop should not happen'
Now extract a method (function):
def test_bot_cannot_drop_off_world_north():
x = 5
y = 0
direction = Direction.NORTH
check_cannot_drop_off_world(x, y, direction)
def check_cannot_drop_off_world(x, y, direction):
world = World(10, 10)
bot_id = world.add_bot(x, y, direction)
bot = world.entity_from_id(bot_id)
block = WorldEntity.block(4, 4)
bot.receive(block)
world.drop_forward(bot, block)
assert bot.has(block), 'drop should not happen'
Inline the variables:
def test_bot_cannot_drop_off_world_north():
check_cannot_drop_off_world(5, 0, Direction.NORTH)
Change the other three tests:
def test_bot_cannot_drop_off_world_north():
check_cannot_drop_off_world(5, 0, Direction.NORTH)
def test_bot_cannot_drop_off_world_east():
check_cannot_drop_off_world(10, 5, Direction.EAST)
def test_bot_cannot_drop_off_world_south():
check_cannot_drop_off_world(5, 10, Direction.SOUTH)
def test_bot_cannot_drop_off_world_west():
check_cannot_drop_off_world(0, 5, Direction.WEST)
I almost like that.
One thing that I do not like is that when I moved these tests over to a separate file, they moved to the top level: they are not embedded in a test class. All my other tests are in test classes. I think for consistency, I’ll change these. All I’ll really have to do is insert self
a lot.
That only took a couple of minutes with some judicious and gratifying multi-cursor editing. I can almost feel the lightning crackling from my fingertips. Commit: improve tests, put tests in a class.
There’s [at least] one potentially confusing thing here. The tests refer to bot
and block
and we might be inclined to think, when we see those words, about our Bot and Block classes on the client side. And, fact is, we were actually using some of those classes some of the time, left over from early versions where we were not yet worrying about client-server.
Of course if this were a real product, we might not have this situation. No, I think we might. I was going to say that we’d have a team working on the world and they wouldn’t be working with any real bots or blocks. But I don’t buy that, even though I just thought it. I just hadn’t thought it through1. Even if I were really just planning to release the world code, I think I’d be building a client-side to my “making app” anyway.
So anyway, we might consider some judicious renaming in our tests, and more to the point, in the World class. We’ll leave that for now. I think we’re about done here.
Summary
In less than an hour, I’ve improved four more tests, reducing their dependence on the client side code, which is a very good thing in spite of my remarks about our making app probably righteously having Bot and Block classes. (Or at least Bot?) And I’ve learned a small lesson, which is that moving test methods to a new file puts them at top level, and we probably don’t want that. In their defense, PyCharm did say top level. I just didn’t think about whether I cared.
If I were condemned to be, oh, 50 years younger and working in a programming situation, I would feel that the occasional hour or so spent improving tests or refactoring code would pay off. YMMV, but it’s probably worth thinking about.
Speaking of “making app”, I was thinking earlier this morning about additional features for our client-side bots. I was thinking that there could be “explorer” bots that would wander around discovering blocks and then marking a scent around them that our existing bots would detect and follow in to the block, saving them a lot of running around. If the explorer bots were faster, or used less food, they might pay off. As things stand now, I am not sure that explorers would be better than just having another working bot.
But then—and this is the real point—I was thinking that we may have enough working on the client side right now. It might be time to dig in on the client server stuff. There would be more education for me in that. And it looks like I’m not likely to get any help on it, so I might as well give it a go.
We’ll see. The good of today is sufficient thereto. See you next time!
-
See what I did there? I couldn’t resist three different words containing “ough”, pronounced in three different ways. At least I didn’t add in “tough”. [Cough] ↩