Testing Overview
At Very Good Ventures, our goal is to achieve 100% test coverage on all projects. Writing tests not only helps to reduce the number of bugs, but also encourages code to be written in a very clean, consistent, and maintainable way. While testing can initially add some additional time to the project, the trade-off is fewer bugs, higher confidence when shipping, and less time spent in QA cycles.
Organize test files
Test files should be organized to match your project file structure.
This my_package
library contains models
and widgets
. The test
folder should copy this structure:
Directorymy_package/
Directorylib/
Directorymodels/
- model_a.dart
- model_b.dart
- models.dart
Directorywidgets/
- widget_1.dart
- widget_2.dart
- widgets.dart
- test/ …
Directorytest/
Directorymodels/
- model_a_test.dart
- model_b_test.dart
Directorywidgets/
- widget_1_test.dart
- widget_2_test.dart
Directorytest/
- model_a_test.dart
- model_b_test.dart
- widgets_test.dart
Note:
models.dart
andwidgets.dart
are barrel files and do not need to be tested.
Assert test results using expect or verify
All tests should have one or more statements at the end of the test asserting the test result using either an expect or verify.
The above test would pass coverage on SomeTappableWidget
, and pass as long as no exception is thrown, but it doesn’t really tell any valuable information about what the widget should do.
Now, we are explicitly testing that we have accessed the onTap
property of SomeTappableWidget
, which makes this test more valuable, because its behavior is also tested.
Use matchers and expectations
Matchers provides better messages in tests and should always be used in expectations.
Use string expression with types
If you’re referencing a type within a test description, use a string expression to ease renaming the type:
If your test or group description only contains a type, consider omitting the string expression:
Descriptive test
Don’t be afraid of being verbose in your tests. Make sure everything is readable, which can make it easier to maintain over time.
Test with a single purpose
Aim to test one scenario per test. You might end up with more tests in the codebase, but this is preferred over creating one single test to cover several cases. This helps with readability and debugging failing tests.
Use keys carefully
Although keys can be an easy way to look for a widget while testing, they tend to be harder to maintain, especially if we use hard-coded keys. Instead, we recommend finding a widget by its type.
Use private mocks
Developers may reuse mocks across different test files. This could lead to undesired behaviors in tests. For example, if you change the default values of a mock in one class, it could effect your test results in another. In order to avoid this, it is better to create private mocks for each test file.
Split your tests by groups
Having multiple tests in a class could cause problems with readability. It is better to split your tests into groups:
- Widget tests: you could potentially group by “renders”, “navigation”, etc.
- Bloc tests: group by the name of the event.
- Repositories and clients: group by name of the method you are testing.
Tip: If your test file starts to become unreadable or unmanageable, consider splitting the file that you are testing into smaller files.
Keep test setup inside a group
When running tests through the very_good
CLI’s optimization, all test files become a single file.
If test setup methods are outside of a group, those setups may cause side effects and make tests fail due to issues that wouldn’t happen when running without the optimization.
In order to avoid such issues, refrain from adding setUp
and setUpAll
(as well as tearDown
and tearDownAll
) methods outside a group:
Shared mutable objects should be initialized per test
We should ensure that shared mutable objects are initialized per test. This avoids the possibility of tests affecting each other, which can lead to flaky tests due to unexpected failures during test parallelization or random ordering.
Avoid using magic strings to tag tests
When tagging tests, avoid using magic strings. Instead, use constants to tag tests. This helps to avoid typos and makes it easier to refactor.
Do not share state between tests
Tests should not share state between them to ensure they remain independent, reliable, and predictable.
When tests share state (such as relying on static members), the order that tests are executed in can cause inconsistent results. Implicitly sharing state between tests means that tests no longer exist in isolation and are influenced by each other. As a result, it can be difficult to identify the root cause of test failures.
Use random test ordering
Running tests in an arbitrary (random) order is a crucial practice to identify and eliminate flaky tests, specially during continuous integration.
Flaky tests are those that pass or fail inconsistently without changes to the codebase, often due to unintended dependencies between tests.
By running tests in random order, these hidden dependencies are more likely to be exposed, as any reliance on the order of test execution becomes clear when tests fail unexpectedly.
This practice ensures that tests do not share state or rely on the side effects of previous tests, leading to a more robust and reliable test suite. Overall, the tests become easier to trust and reduce debugging time caused by intermittent test failures.