TDD and Randomness

Nicolas Savoini
The Startup
Published in
4 min readAug 4, 2020
Photo by Jonathan Petersson on Unsplash

A year ago, I discovered Test-Driven Development. I started with simple functions, then functionalities. Now it’s time to do a full project.

I started to code a famous European game: 421. It’s a quick fun game with two players, three dice and 15 tokens. My dad and I love it.
In the first phase, the players roll their dice once. The player with the lower combination receives one or more tokens. The process repeats until there are no more tokens to distribute, at which point phase two begins. Players can then roll their dice up to three times. When both players have their final combination, the winner gives tokens to the loser. The game ends when one player has no more tokens.

We will use phase one of the game to illustrate the issue between TDD and a random event.

The problem

We already coded features such as player, dice, combination (generation and comparison). We can write our first test to see it all coming together.

We chose the following example:

  • Player one rolls: 3, 2, 5
  • Player two rolls: 4, 4, 5

Player two (544 > 532) wins, so player one should receive a token.

We write our test.

Then our Play() method.

Launch the test once, and maybe it will pass, maybe it won’t. Because of the random dice, you can’t expect consistent test behavior.

The solution

To solve this issue, we will first need to isolate the random generation into its own method.

The second step is to move the random function to the top of the Play() method.

Now that the random call is the first line of Play(), we can extract it to a parameter.

We could start testing, but because we changed the signature of the Play() method to force a combination, we will have to do so every time we call the function, even in the regular game.

Let’s change that.

We rename the Play() method to PlayCombination() and set it private. The new Play() method is just a wrapper of the old method. The Play() method still has the same signature and the same functionality. It has no parameter, as it should, and uses a random combination. We have the PlayDebug() method for our tests, which takes a specific configuration, as we wanted.

Launch the test, and it will pass. launch it, again and again, it will keep passing. We work around the randomness part of our code, and our tests thanks us for it. We can continue to add tests for edge cases.

For example, what if the game ends in one turn?

Can you imagine how many tests we would have had to launch to get those two combinations randomly?

Random information creates a barrier for TDD and testing in general. You cannot expect the same result for every execution.
We saw that the way around this is to extract the random part, then duplicate it with a non-random call. It will allow you to do the necessary tests.

This method solves our problem but introduces a dangerous side-effect.

We have a PlayDebug() method in our production code. It’s not great because people can use it for something else than testing: cheating.

If someone uses your code, we could fall into the following scenario. The developer (player) uses the Play() method unless he might lose with the next roll. In this case, switch to the PlayDebug() function and rest assured that you win the game.

We are not responsible for others’ work, but I have a problem opening my code to such vulnerabilities just for testing.

Testing should make your code better, not create holes in it.

To get around this issue when your code is compiled first then shared, you can use the #if DEBUG flag.

The flag will prevent this part of the code from being included in the final product.

If you share the source code, you cannot prevent someone else from creating a cheating method, but, at least, you should avoid including it in the package.
For that, you can take advantage of the .gitignore file.
First, create a specific debug folder for your code and tests.
Add the PlayDebug() method into an extension, then add the extension file in the debug folder. Do the same for the test.
Finally, use the .gitignore file to avoid publishing the debug folders.

I realize that by sharing the source code, you always leave it open to all kinds of change, that is the goal of sharing. But it doesn’t mean we have to help.

If you want to know more about Test-Driven Development (TDD), you can check my previous article.

--

--

Nicolas Savoini
The Startup

Passionate geek, I have few apps on the AppleStore and a lot of ideas:) write about Tech and Life. nicolas.savoini@mac.com