Sparround

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 / matcherWhat it checksExample
`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.instance destroys isolation and makes tests order-dependent.
  • Inject time and randomness: DateTime.now() and Random() inside a function make tests flaky; a DateTime Function() now parameter and a seeded Random are 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