Writing your first test in Dart

Stephan E.G. Veenstra's photo
Stephan E.G. Veenstra
·Jul 6, 2022·

6 min read

Writing your first test in Dart

Table of contents

  • Structuring the tests
  • Our first test
  • Adding another test
  • What's next?

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

 
Share this