TDD – Create Game Logic Using Test-First Programming

Architecturetddunit testing

I'm implementing a simple command line game using TDD principles and Unit Testing. My goal is to implement the whole game using Test-First Programming, so every behaviour/class is created from a test.
I've already implemented things like a Player (the game needs a player), the Enemy (the player fights the enemy) and other stuff but now that I have to implement the Game logic itself, which actually takes all these classes and combines them together to achieve the functionality, I'm stuck.
The problem is that the Game itself should just expose a Run() method and nothing more. This is because it's the game itself that runs its logic and not someone else from the outside that tells it what to do.

Here is some pseudo-code,

class Game
{
    Game(Player p, EnemyFactory f)
    {
       this._player = p;
       //and so on...
    }

    public void Run()
    {
       // All the game logic goes here
       // In example:
       int totalEnemies = 5;
       while(_player.IsAlive && totalEnemies > 0)
       {
           Enemy enemy = _enemyFactory.CreateEnemy();
           _player.Fight(enemy);
           // other stuff
           totalEnemies--;
       }

       GameOver();
    }

    private GameOver()
    {
       //other stuff
    }
}

As you can see, the main logic is inside the Run function, which makes sense to expose as public, so that the Main() function that creates a Game class also calls Run(), but since all the logic will be a bunch of private functions, how do I create it from the tests first?
The only thing that comes to my mind is or to use Mock, which I really don't want to do, or to expose some public functionality, but I don't want to create public behaviours that will be called only from the tests, because it's not a good practice.

Any idea please?

Best Answer

I'm implementing a simple command line game

I frequently use this as a starting point for TDD exercises. It's a really good choice, as it forces you to discover important distinctions between logic and effects.

As you can see, the main logic it's inside the Run function, which makes sense to expose as public, so that the Main() function that creates a Game class also calls Run(), but since all the logic will be a bunch of private functions, how do I create it from the tests first?

Your program is going to have a natural break between two parts - I/O and logic. Testing I/O is harder - it lives higher up the test pyramid than the logic tests. This is in part because the IO system tends to be shared; you can't as easily run two tests concurrently without them interfering with each other.

So you are likely to end up with some tests that look like

cat input | my-game > actualOutput
diff actualOutput expectedOutput

If you read Growing Object Oriented Software, Guided By Tests, you find that Freeman and Price will talk about dedicating their initial work to a test like this run as part of their build/deploy pipeline. It won't be a fancy implementation of the game yet (think hello world), but it will be enough to demonstrate that the project delivery system works.

Moving down the pyramid, we're going to find some tests that use test doubles; in this case, a "stub" or a "fake" for STDIN and a "mock" or "spy" for STDOUT. So imagining an implementation of Game.main, it might look like:

void main (String [] args) {
    Game.run(System.in, System.out);
}

or

void main (String [] args) {
    Game g = new Game(System.in, System.out);
    g.run();
}

You don't normally try to "unit test" this main method, because it is directly coupled to the shared instances of STDIN and STDOUT. Instead, the unit tests will measure the behavior of run using test doubles. We'll be asserting on information collected by the implementation of STDOUT that we provide during the test.

This is an important idea in TDD - we've deliberately designed the interface of game such that it can be tested in a more cost effective way.

JB Rainsberger wrote:

If we want to introduce indirection, then we ought to introduce the smallest indirection possible. And we absolutely must try to introduce better abstraction.

What we've done here is introduce a seam, which is the indirection that gives us control of the subject in the test environment. What we want to be discovering as we iterate on the internal design is better abstractions.

Note that, to this point, we haven't really done anything to introduce Player, Enemy, or any of the other domain abstractions that we expect to use. Those concepts are things that we expect to discover and extract during the refactoring step. Once you have discovered them, and feel that their API is sufficiently stable, you can start writing tests that specifically measure those elements.

Which is to say, the tests of the Game, working seam to seam, tend to be too big, in this sense: they span a lot of distinct behaviors. Because there are so many behaviors within the scope of the test, those tests tend to be "brittle"; we decide one little bit of behavior should be slightly different, and the entire test falls over.

The only thing that comes to my mind is or to use Mock, which I really don't want to

Well, then lets go a level deeper; STDIN and STDOUT are sources of messages to and from the IO System. From a very high level, we have some thing like

String [] lines = read(this.stdin)
String [] output = this.gameLogic(lines)
writeTo(this.stdout, output)

gameLogic here doesn't depend on the IO system at all; it only cares about the data structure passed to it. So you can test everything that is interesting and unique about your game in memory, without needing to worry about the IO system at all.

You probably wouldn't choose this API, because we like to think of games being interactive. Instead, you'd probably end up with something like a state machine

while(game.running) {
    String line = nextLine(this.stdin)

    game.onLine(line)
    String output = game.output()

    writeTo(this.stdout, output)
}

Again, that middle section is just passing in memory data structures into and out of the game logic that you actually care about. You "discover" this sort of API while iterating on the design that uses the mocks, but once this API is stable you can start writing tests directly against the API.

Gary Bernhardt's Boundaries talk is a great introduction to this concept.

From this point, it's turtles all the way down; you continue to add new tests for desired behavior, and iterate on your internal design, and as concepts like Player and Enemy begin to stabilize, you start writing even smaller tests directly against those elements.

you are saying that I started from the "smaller" classes that are composing the Game, instead of starting from the Game itself. I agree, I did that because I was stuck with this Run() problem.

Right. And to be fair, a lot of TDD literature starts from the inside and works outwards -- the members of the "Detroit school" already had the habits to separate the domain model from the plumbing, so they had a tendency to start from the middle.

The other thing you are suggesting is basically use an external logic passed through the game (that can be mocked) to see if the game behaves in a certain way based on what is set on that logic.

No, I'm not suggesting that; although I will admit that it works. What I'm actually suggesting is slightly different -- that if you have the right arrangement of tests, you can iterate on your internal design until you have a "CLI Framework" component and a core game logic component. The framework will be something that you can re-use for your next CLI app; the game logic will be something you can re-use when you decide your game should be accessible via a web api.

What I have found, in practice, is that as you become familiar with the basic isolation patterns, you tend to start "closer" to the interesting bits. So, for instance, if I'm writing an interactive CLI game, or a pipes-and-filters component, then I will just jump right past all of the plumbing tests and write the API signature straight from my head.

On the other hand, as soon as you introduce something like "we also need to be able to save and load games from the file system", I need to zoom out a few layers and work my way back inwards more slowly, with more tests to reach my confidence threshold.