Writing tests in Dart is super easy. Everything you need to get started you get right out of the box. Every Dart (and Flutter) project comes with an example test to get you started.
This article is the fourth in the Introduction to TDD series in which we are creating a Tic Tac Toe logic package using Test Driven Development.
In the previous article we've defined the following API for our package:
enum Player { one, two }
enum Status { p1Turn, p2Turn, p1Win, p2Win, draw }
abstract class TicTacToeGameState {
List<Player?> get fields;
Status get status;
TicTacToeGameState claimField(int index);
}
In this article we will write our first test and implement the corresponding code to make the test pass.
Structuring the tests
We will separate our tests from our implementation code by putting them in a separate directory.
The test
directory, which was generated when we created the project, already contains an example test. We won't be using this file, so it can be deleted. We will add our new test file to this test directory.
It's common practice to mirror the files under the src
directory for the test files. So when we have a file under src
, we will have a corresponding test file under test
. For better distinguishing the implementation files from the test files we end test files with test.dart
.
For our case it would look like this:
/lib
/src
/tic_tac_toe_game_state.dart // game state implementation
/tic_tac_toe_game.dart // the library file
/test
/tic_tac_toe_game_state_test.dart // game state tests
Our first test
In the very first article of this series I suggested to start with the low hanging fruits. These are the tests that require the least effort, like getter-fields and (simple) constructors.
Covering our first requirement
In the second article of the series (Preparing for development) we've listed the rules of Tic Tac Toe. One of these rules is:
The game is played on a three-by-three grid (starts out empty).
So initially, when we create a new game state, fields
should return a list containing nine (three times three) empty fields. We can write a test for this!
Testing the initial game state
It has been decided. We are going to test the initial state of the game by writing a test that expects a list of nine empty fields. The test will look like this:
void main() {
group(
'fields',
() {
test(
'initial state should return 9 empty fields',
() {
final gameState = TicTacToeGameState();
expect(
gameState.fields,
<Player?>[null,null,null,null,null,null,null,null,null],
);
},
);
},
);
}
We've created a group named 'fields' for the property that we are testing. Grouping tests is useful because it allows you to run all the tests for the thing you are currently working on.
Then we create the test
with a descriptive name about what we are expecting. The test itself is rather simple. We create a new instance of TicTacToeGameState
and then check if the fields
property returns the expected value (a list of nine null
values).
However, we are not able to run this test yet, since we can't instantiate an abstract class, which TicTacToeGameState
currently is.
Writing the implementation
To make the test able to run and pass we have to start writing the implementation. Some people prefer to write the implementation logic as a new class and make that new class implement the interface.
In our case I think that's a little bit overkill, so we're going to change our abstract class into a regular class. We do this by simply removing the abstract
keyword.
enum Player { one, two }
enum Status { p1Turn, p2Turn, p1Win, p2Win, draw }
class TicTacToeGameState {
List<Player?> get fields;
Status get status;
TicTacToeGameState claimField(int index);
}
Now your IDE will probably complain that you have to implement the getters and the function. Since we only care about the fields
getter for our first test, and the rule is to write as little implementation as possible to make the test pass, we can end up with the following:
enum Player { one, two }
enum Status { p1Turn, p2Turn, p1Win, p2Win, draw }
class TicTacToeGameState {
List<Player?> get fields => [null,null,null,null,null,null,null,null,null];
Status get status => throw 'not implemented';
TicTacToeGameState claimField(int index) => throw 'not implemented';
}
We made fields
return exactly what is expected from the test, a list of nine null
values. The others just throw a 'not implemented' message for now just to keep the compiler happy. They won't cause a problem since they won't be called yet.
Running the test
Now that we have written our first implementation, we can run the test to see if it passes.
Most of the IDE's like VScode and Android Studio have the ability to run the tests directly from code and show the results in a nice UI. I will use the Dart commands in the terminal to stay IDE independent. Let's give it a try!
$ dart test
00:01 +1: All tests passed!
Running dart test
(or flutter test
) will make Dart discover all the tests and run them. In our case the test passes, since we return exactly what was expected by the test.
Great job, but we can do more!
Adding another test
Our fields
getter doesn't do much, and what it does, we've tested. But, there is one more test that we can write that can be of great value.
Protecting the property
When the consumer of our package gets the fields
from a TicTacToeGameState
it is meant to be used for representing the current state. The return type of fields
is a List
, and a List
in Dart has functions to add
and remove
items.
This is a problem, because this means that the consumer could try to bypass our claimField
function by updating the fields
directly.
Let's first write the test for this problem:
test(
'changing the returned list should not alter the inner state',
() {
final gameState = TicTacToeGameState();
gameState.fields[0] = Player.one;
expect(
gameState.fields,
[null, null, null, null, null, null, null, null, null],
);
},
);
The test is almost the same as our first. With the only difference that in this test we're trying to alter the fields
directly by setting the first entry to Player.one
. We expect this to have no effect.
Running our second test
Now we're going to run the test to see if it fails. We will run the dart test
command with the name
parameter so we will only run the test we are currently working on.
$ dart test --name="changing the returned list should not alter the inner state"
00:01 +1: All tests passed!
Wait what?! It passed? Well, it does make sense if we take a look at our implementation. fields
currently returns an empty List
every time it gets called. So yeah, our test passes because the second time we get fields
we get a different one than the one we've just tried to manipulate.
Just because we didn't have to write any new code to make the test pass, doesn't mean this test is useless. Code can be refactored in the future so we've now secured it for possible future changes, well done!
What's next?
We've now covered fields
with two tests. The first checks if the initial value returns the expected value and the second checks if we're not able to alter the fields
directly.
In the next article we will continue with the other property, status
. We are going to have to find a way to create different states without actually playing the game to be able to test all the different outcomes.
In the next article I will show you how you could do that!
Next article: coming soon