Testing in Dart
Dart tests are written with package:test, live in test/ and end in _test.dart — that is the pattern dart test looks for.
The structure has four elements:
test('...', () { ... })— one case; its name should describe the behaviour ("an empty cart totals zero"), not the method ("testTotal").group('...', () { ... })— logical grouping; the names concatenate in the report, making a failure easier to locate.setUp/tearDown— run before/after every test. Creating the objects under test here is the basis of isolation: every test starts from a clean state.setUpAll/tearDownAll— once per group. For expensive, immutable resources (e.g. reading a fixture file once). Keeping mutable state here is the classic cause of leakage between tests.
tearDown runs even when a test fails. A frequently tidier alternative is addTearDown(...): you register the cleanup on the same line as the setup, so it is harder to forget.
| API / matcher | What it checks | Example |
|---|---|---|
| `equals(x)` (or just `x`) | `==` equality; for collections it compares **element by element** | `expect(cart.total, 500)` |
| `isA<T>()` | The type; can also check fields via `.having(...)` | `expect(e, isA<PaymentDeclined>().having((x) => x.code, 'code', '402'))` |
| `throwsA(matcher)` | That a function throws — the argument must be a **closure, not a call** | `expect(() => parse('x'), throwsA(isA<FormatException>()))` |
| `completion(matcher)` | That a Future completes successfully, and its value | `await expectLater(fetchAge(), completion(equals(42)))` |
| `emitsInOrder([...])` | The order of events a stream emits; combines with `emitsError`, `emitsDone`, `neverEmits` | `await expectLater(s, emitsInOrder([1, 2, emitsDone]))` |
| `predicate((x) => ...)` | An arbitrary condition — when no built-in matcher fits | `expect(user, predicate<User>((u) => u.age >= 18, 'is adult'))` |
| `closeTo(v, delta)` | A `double` comparison with tolerance — `0.1 + 0.2 == 0.3` is false | `expect(rate, closeTo(0.3, 1e-9))` |
| `expectLater(...)` | The Future-returning version of `expect` for async matchers — **must be awaited** | `await expectLater(stream, emits(1))` |
Async tests have one rule: never leave anything unawaited. Make the test function async and await every asynchronous expectation. Writing expectLater(...) instead of await expectLater(...) lets the test finish before the check does — a false green, and the biggest time sink in real projects.
Making code testable matters more than writing tests. Three practical rules:
- Constructor injection: dependencies (HTTP client, repository, clock, randomness) arrive as parameters instead of being
new-ed inside. Only then can a test substitute them. - Avoid static singletons:
Db.instancedestroys isolation and makes tests order-dependent. - Inject time and randomness:
DateTime.now()andRandom()inside a function make tests flaky; aDateTime Function() nowparameter and a seededRandomare enough.
Fake or mock? A fake is a working, simplified implementation of the interface (an in-memory repository). A mock records calls and returns canned answers (usually via package:mockito or mocktail). Default to fakes: they preserve real behaviour and survive refactoring. Mocks earn their place when the interaction is the thing under test: "was the analytics event sent exactly once".
Interview tip: "What coverage percentage should you have?" is a trap. Don't name a number — name a criterion. A strong answer: "We measure with dart test --coverage=coverage and convert to LCOV with package:coverage. But coverage only shows which lines ran, not which were verified — an assertion-free test still produces coverage. So instead of an absolute number I watch two things: that new code doesn't lower coverage, and that the critical domain logic (pricing, validation, state machines) is actually covered." That answer beats saying "80%" by a wide margin.
📚 Sources and documentation
- dart testofficialdart.dev
- The test packagepub.dev