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 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.
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
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:or
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 ofrun
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:
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.
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
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
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
andEnemy
begin to stabilize, you start writing even smaller tests directly against those elements.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.
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.