2

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?

6

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.

  • Thanks for the reply. So if I understand correctly, 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. 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. Is that correct? – Maloooo Jul 22 at 12:53
  • 2
    This is a good answer - and can also be applied to working with engines like Unity - where your MonoBehaviour would be a mini I/O wrapper (events in, behaviour out) around logic for a particular game component. The key lies in making those wrappers as thin as possible so they deal only with the "impure" outside world and map cleanly to a "pure" test-driven model. After that it's just plain old TDD. – Ant P Jul 22 at 12:59
  • Just a small nitpick - Mocks are scary (because they test much more implementation than behaviour and as such are brittle), but one should not be dissuaded from using more simple kinds of Test Doubles, mainly Stubs (as those two are often misunderstood as being two names for the same thing). – herby Jul 22 at 13:14
3

Ok, first thing first: although test-driven development and test-first development may look as synonyms, they have a subtle difference, which is most emphasized by word "driven". Test-first development is just a technical practice of writing test before the implementation. Test-driven development goes a bit further and places testing in the driver seat. It is much stronger and bring more benefits, but requires different discipline and more open mind.

If you're just trying test-first development, ignore the rest. I'll assume you are going to try test-driven development.

If you practice real TDD, you let your whole development to be driven by testing. From your "The problem is that the Game itself should just expose a Run() method and nothing more." I feel an assumption where you are not doing that.

Are you willing to let not just the implementation, but the whole design of your game to be slave to the testing? And not only to the test that are written, but also (what is more important) to the test which is just being written?

If yes, TDD has different question that what you essentially posted (and a different answer, then). The question is "how to be able to write a test for this behaviour"? Its answer is "if you cannot write a test with your current design, easily, it's the design which is wrong. Change it so you can write a test, easily."

So maybe your game needs not to run in a single black-box while loop. Because that is definitely not testable easily. Maybe it should do 'while (this.IsRunning) this.Step();' and you can write test more easily, then. And maybe you should not have 'Game()' constructor with Init() method that creates the player and the enemy, because that makes it not testable easily, but you should instead employ DI (see How to unit test that the correct implementation of an interface is used? question of mine where I struggled with similar problem - how to test the code that glues other pieces).

In short, if you first plan you design up front and then try to employ test-first approach to its pieces, you don't do true TDD, you are using much weaker practice, still useful, but as you see, limited.

If you make all your development depend on the testing, including changing it when it is not testable, to be testable, you do proper TDD.

Hint why that may really be helpful to do it that way: https://vimeo.com/15007792

EDIT: To add one more thing, it's often the case in TDD that you go top-down, not bottom-up. You mentioned that you already have your Player and Enemy classes ready, done via test-first approach. But how did you know what will be their API? You must have designed it up-front. As such, you implemented a speculative code. More YAGNI / TDD approach would be to start with GameLogic first, using stubbed Player and Enemy. Those tests would test your game logic, but at the same time they will sketch the (optimal, testing-wise) API of Player and Enemy. Only after the client (GameLogic) is ready, should the dependencies (Player, Enemy) started to be created, because now the API is known from its client, so you will not do speculative work. This is one more dimension where testing actually drives the design, not vice versa.

  • Thanks for the answer. I agree with you, I was probably biased by the fact that in game dev you usually have an Update method that does all the logic of the game. I'm passing the Player to the Game using the constructor so that I can use a Fake or similar in other tests and I'm also passing an EnemyFactory to the Game that creates the Enemies, so I just edited my question to be more clear. So you are suggesting to move the logic into a Step() method, and that one would be public? Or pass into Game some external logic that has the Step method? and test that one? – Maloooo Jul 22 at 12:34
  • Maybe "having a public interface to run the game" and "doing the game logic" are two responsibilities. If you will have GameLogic class which can have more structured (and public, that is, testable) interface (Start(), HandleFoo(), HandleBar(), Finish(), ... making things up) and Game which would just a facade that calls proper things on GameLogic in its Run method, you may have these two reponsibilities divided, and then testing naturally is easier for both parts. – herby Jul 22 at 13:08
  • Actually, I added one more things, see the edit. – herby Jul 22 at 16:04
0

The problem is that the Game itself should just expose a Run() method and nothing more.

That is an implementation detail, not necessarily something derived from test-driven development. What does your first test about the Game actually test? Maybe that you can actually create a Game object given a Player object; OK, that test passes, you've implemented that constructor. But then what?

What test(s) did you write at this point, and what are you expecting the results to be? That should drive the implementation of the Game class, and point to where you may need some additional public methods.

Take a piece of the game logic you want to implement and write a test for it - setting up what is needed (create a Player and Game object), Running the Game so it runs that logic, and then checking that it did what you wanted it to (check some properties of the Player?).

Without knowing what your game is about at all, maybe one piece of game logic you want to test is that the player can move around. So you set up the player and game, put the player in a location (is that done by the game or the player; you may need a new method in the Game class to do that), and then move the player (does the game move the player? How does the game move the player?). Finally, check that the location of the player is what you expect (accessed by the Player or Game)?

Making methods public simply so they can be tested is not a great idea, I agree. However, consider that you need to be able to manipulate the objects you create to respond to what happens in the game. Making methods public so that you can implement the game logic is different than making them public just so they can be tested.

  • Thanks for the reply. I agree but if there is a public method that implements the game logic but it's only called from the tests in the codebase, then it shouldn't be public at all. Unless, I make an IGameInterface that Game will implement and then it's ok to have that method public, since it's coming from an interface. It this what you meant? – Maloooo Jul 22 at 12:39
  • How would you as a developer know that the game logic was implemented correctly? What objects or properties would you check? Is it something in the Game itself, or something in Player? – mmathis Jul 22 at 13:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.