Writing tests

  • All new functionality must be tested, if at all possible. When fixing a bug, tests must be added to ensure that this bug would not reappear in the future.

  • Run melos run coverage to execute all tests in the “coverage” mode. The results will be saved in the coverage/index.html file, which can be opened in a browser. Try to achieve 100% coverage for any new functionality added.

  • Every source file should have a corresponding test file, with the _test suffix. For example, if you’re making a SpookyEffect and the source file is src/effects/spooky_effect.dart, then the test file should be test/effects/spooky_effect_test.dart mirroring the source directory.

  • The test file should contain a main() function with a single group() whose name matches the name of the class being tested. If the source file contains multiple public classes, then each of them should have its own group. For example:

    void main() {
      group('SpookyEffect', () {
        // tests here
      });
    }
    
  • For a larger class, multiple groups can be created inside the top-level group, allowing to navigate the test suite easier. The names of the nested groups should be capitalized.

  • The names of the individual tests should normally start with a lowercase.

  • Often, you would need to define multiple helper classes to run the tests. Such classes should be private (start with an underscore), and placed at the end of the file. The reason for this is that whenever some test breaks, the first thing one needs to do is to go into the test file and run all the tests. Having the main() function at the top of the file makes this process much easier.

Types of tests

Simple tests

test('the name of the test', () {
  expect(...);
});

This is the simplest kind of test available, and also the fastest. Use these tests for checking some classes/methods that can function in isolation from the rest of the Flame framework.

FlameGame tests

It is very common to want to have a FlameGame instance inside a test, so that you can add some components to it and verify various behaviors. The following approach is recommended:

testWithFlameGame('the name of the test', (game) async {
  game.add(...);
  await game.ready();

  expect(...);
});

Here the game instance that is passed to the test body is a fully initialized game that behaves as if it was mounted to a GameWidget. The game.ready() method waits until all the scheduled components are loaded and mounted to the component tree.

The time within the game can be advanced with game.update(dt).

If you need to have a custom game inside this test (say, a game with some mixin), then use

testWithGame<_MyGame>(
  'the name of the test',
  _MyGame.new,
  (game) async {
    // test body...
  },
);

Widget tests

Sometimes having a “naked” FlameGame is insufficient, and you want to have access to the Flutter infrastructure as well. That is, to have a game mounted into a real GameWidget embedded into an actual Flutter framework. In such cases, use

testWidgets('test name', (tester) async {
  final game = _MyGame();
  await tester.pumpWidget(GameWidget(game: game));
  await tester.pump();
  await tester.pump();

  // At this point the game is fully initialized, and you can run your checks
  // against it.
  expect(...);

  // Equivalent to game.update(0)
  await tester.pump();

  // Advances in-game time by 20 milliseconds
  await tester.pump(const Duration(milliseconds: 20));
});

There are some additional methods available on the tester controller, for example in order to simulate taps, or drags, or key presses.

Golden tests

These tests verify that things render as intended. The process of creating a golden test is simple:

  1. Write the test, using the following template:

    testGolden(
      'the name of the test',
      (game) async {
         // Set up the game by adding the necessary components
         // You can add `expect()` checks here too, if you want to
      },
      size: Vector2(300, 200),
      goldenFile: '.../_goldens/my_test_file.png',
    );
    

    Here the size parameter determines the size of the game canvas and of the output image. The goldenFile parameter is the name of the file where you want to store the “golden” results. This should be a relative path to the test/_goldens directory, starting from your test file.

  2. Run

    flutter test --update-goldens
    

    this would create the golden file for the first time. Open the file to verify that it renders exactly as you intended. If not, then delete the file and go back to step 1.

  3. Subsequent runs of flutter test will check whether the output of the golden test matches the saved golden file. If not, Flutter will save the image-diff files into the failures/ directory where your test is located.

Note

Avoid using text in your golden tests – it does not render reliably across different platforms, due to font discrepancies and differences in anti-aliasing algorithms.

Random tests

These are the tests that use a random number generator in order to construct a randomized input and then check its correctness. Use as follows:

testRandom('test name', (Random random) {
  // Use [random] to generate random input
});

You can add repeatCount: 1000 parameter to run this test the specified number of times, each one with a different seed. It is useful to run a high repeatCount when developing the test, to ensure that it doesn’t break. However, when submitting the test to the main repository, avoid repeatCounts higher than 10.

If the test breaks at some particular seed, then that seed will be shown in the test output. Add it as the seed: NNN parameter to your test, and you’ll be able to run it for the same seed as long as you need until the test is fixed. Do not leave the seed: parameter when submitting your code, as it defeats the purpose of having the test randomized.