Test-driving Tetris from the outside in (2)

Sep 12, 2019 tdd tetris rust | Prev | Next

Before we begin:

I want to go into a bit more detail on how everything is put together by way of a more complex example. So, in Tetris, a line goes away as soon at it is full, right?

What follows is the first test I wrote to drive this behavior. It is the simplest one in vanishing_lines_test.rs:

#[test]
fn single_line_vanishes() {
    let mut game = TestableGame::init()
        .with_brick_type_encoding()
        .with_brick_sequence(vec![i_block(), z_block()])
        .with_field(vec![
            "..........",
            "..........",
            "o....zzool",
        ])
        .build();

    game.verify_frame_after(0, vec![
        ".iiii.....",
        "..........",
        "o....zzool",
    ]);

    game.tick(100);
    game.verify_frame_after(200, vec![
        "..........",
        "..........",
        "oiiiizzool",
    ]);

    game.verify_frame_after(300, vec![
        ".zz.......",
        "..zz......",
        "..........",
    ]);
}

This test does roughly this:

It depends on two key ingredients:

The first ingredient is provided by a struct called GameBuilder which is created by the call to Game::init - the details are not really important here, but suffice it to say that the builder pattern comes in very handy when the game needs to be tweaked for different testing scenarios.

In this case, we are saying, well, the game field is really small, like 3 rows, and it already has some stuff in it. Also, please spawn blocks in a specific sequence (I-block then Z-block).

Secondly, when the game renders itself in the test, it uses a struct called ToStringRenderer, which implements a trait called TRenderer:

pub trait TRenderer {
    fn clear(&mut self);
    fn draw_bricklet_at(&mut self, x: u8, y: u8);
}
And so the game will know how to render a “bricklet”, which is what I call one square part of a Tetris brick or block.
This is a very domain specific trait. It allows us to care only about the abstraction of a brick being a collection of little squares. How these squares appear on a screen is of no concern here. Heck, we can even choose what the screen is. And we say it is a bunch of strings. Hooray!

With this in hand, we can just say that, hey, a bricklet is now just a character based on the block type (i-block, t-block, o-block, etc) in a string, and it is at some position in an array of strings, and we can take this array and write an assertion against it, right there in the code. And that assertion will have a very natural look, being a visual representation of our assumption.

That verify_frame_after function is just a convenience function, it looks like this:

pub fn verify_frame_after(&mut self, now: u64, expected_frame: Vec<&str>) {
    self.game.tick(now);
    self.render();
    self.renderer.assert_frame(expected_frame);
}

pub fn render(&mut self) {
    self.game.render(&mut self.renderer);
}

So as you can see here, the ToStringRenderer has its own assert function, which is fine because it is only used in tests anyway.

One last neat thing: assert_frame will internally convert both the actual and the expected frame into a string divived by newline characters. This enables an IDE like Intellij IDEA to provide us with an actual diff, should the test fail. Have a look at this:

So if we somehow messed up our “vanishing lines” algorithm, this is what a test failure would look like.

And I think that is pretty neat.