skip to content

Unit Test Runner

package:test structures suites with test, group and setUp hooks, asserts with expect and matchers, and runs them on the VM or in browsers via dart test. Interviewers probe async and stream tests.

part ofDartoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Dart's package:test, how do test, group, setUp, tearDown and setUpAll organise a suite, and in what order do the hooks run?

level: juniorimportance: must knowfreq 55%

answer

  1. group prefixes the test name
  2. setUp runs before every test
  3. tearDown runs even after a failure
  4. outer setUp first, inner tearDown first
  5. setUpAll once, easy to leak state

basics

~20 s

test declares one case; group nests cases under a name prefix. setUp runs before each test, outer groups first; tearDown runs after each test, even a failing one, inner groups first; setUpAll and tearDownAll run once per group.

solid answer

~50 s

`test('description', body)` registers one case; the body may be `async`. `group('DateParser', () { ... })` nests tests, and its description is prepended to theirs, so reports read `DateParser parses ISO dates`. `setUp` runs before **each** test in the group where it is declared, after any `setUp` in parent groups, and callbacks in one group run in declaration order. `tearDown` runs after each test **even if it failed**, before parent-group tear-downs, and in reverse declaration order. `setUpAll`/`tearDownAll` run once around all tests of a group and are skipped if none of its tests run; the package's own docs warn they easily create hidden dependencies between tests. `addTearDown` registers cleanup from inside a running test, runs before the `tearDown` callbacks, and fits resources created mid-test. Files named `*_test.dart` under `test/` are what `dart test` picks up by default.

code

dart · 18 lines
dart
import 'package:test/test.dart';

void main() {
  final log = <String>[];
  setUp(() => log.add('top setUp'));
  tearDown(() => log.add('top tearDown'));

  group('outer', () {
    setUp(() => log.add('outer setUp'));
    tearDown(() => log.add('outer tearDown'));

    test('case', () {
      addTearDown(() => log.add('addTearDown'));
      log.add('body');
    });
  });
  // log: top setUp, outer setUp, body, addTearDown, outer tearDown, top tearDown
}

go deeper

for a junior

Recall test, group, expect and the four hooks, and that tearDown still runs when a test fails.

for a middle

Explain the exact order across nested groups, where addTearDown fits, and why setUpAll is skipped when no test in the group runs.

for a senior

Show you keep tests independent: fresh fixtures in setUp, shared ones only when read-only and costly, and shuffled order in CI to catch coupling.

for a principal

Weigh suite speed from shared fixtures against the debugging cost of order-dependent failures across a large codebase.

## The building blocks `package:test` is the standard Dart test framework, used directly for Dart packages and underneath `flutter_test` for Flutter. A test file is an ordinary Dart program whose `main()` **declares** tests; the runner then executes them. - **`test(description, body)`** declares one test case. `body` can return a `Future`; the runner waits for it. - **`group(description, body)`** nests tests. The group's description is prepended to every test inside it, and hooks declared inside apply only to that group. - **`expect(actual, matcher)`** asserts; a plain value as `matcher` is wrapped in `equals`. ```dart import 'package:test/test.dart'; import 'package:dates/dates.dart'; void main() { group('IsoDateParser', () { late IsoDateParser parser; setUp(() => parser = IsoDateParser()); test('parses a calendar date', () { expect(parser.parse('2026-09-29'), equals(DateTime(2026, 9, 29))); }); test('keeps the UTC flag for a Z suffix', () { expect(parser.parse('2026-09-29T10:00:00Z').isUtc, isTrue); }); }); } ``` The report shows `IsoDateParser parses a calendar date`, which is why group names are nouns and test names read as behaviour. ## The four hooks and their order | Hook | Runs | Order among several | |---|---|---| | `setUp` | before each test in its group | parent groups first, then declaration order | | `tearDown` | after each test, **even if it failed** | own group before parents, reverse declaration order | | `setUpAll` | once before the group's first test | parent groups first | | `tearDownAll` | once after the group's last test | own group before parents | For a test nested two groups deep, the sequence is: 1. top-level `setUp` callbacks; 2. outer group `setUp`; 3. inner group `setUp`; 4. the test body; 5. any `addTearDown` callbacks, most recent first; 6. inner group `tearDown`; 7. outer group `tearDown`; 8. top-level `tearDown`. `setUpAll` and `tearDownAll` wrap the whole group once and **do not run if none of the group's tests run**, for example when a name filter excludes them all. ## addTearDown `addTearDown(callback)` is called **inside** a running test (or a helper it calls). It registers cleanup for that test only, runs **before** the `tearDown` callbacks, and follows last-in, first-out order. It is the tidy choice when a helper creates a resource halfway through a test: ```dart // needs import 'dart:io'; test('reads dates from a temp file', () async { final dir = await Directory.systemTemp.createTemp(); addTearDown(() => dir.delete(recursive: true)); // ... }); ``` Calling it outside a test throws a `StateError`. Called from `setUpAll` or `tearDownAll`, it runs after all tests in the suite instead. ## Why setUp beats setUpAll by default `setUp` gives each test a **fresh** fixture, so tests cannot affect each other and can run in any order. `setUpAll` shares one fixture across a group, which is faster for expensive resources (a database file, a spawned process) but lets one test's mutation leak into the next. package:test's own documentation recommends `setUp` and reserves `setUpAll` for callbacks that are prohibitively slow. Shared state of this kind is what the runner's `--test-randomize-ordering-seed` option, which shuffles test order, is good at exposing. ## Where tests live - The `test` package is a **dev dependency**. - Files end in `_test.dart` and live under `test/`; `dart test` with no path finds them recursively. - Run one file with `dart test test/iso_date_parser_test.dart`. ## Mistakes interviewers look for - Declaring shared state as a top-level `final` initialised once, instead of resetting it in `setUp`. - Assuming `tearDown` is skipped when a test fails, and putting cleanup at the end of the test body instead. - Nesting groups only for indentation, while hooks in them unexpectedly apply to every test inside. ## A worked layout for a date library A test file for a date-parsing library usually mirrors the public API: 1. a top-level `group('IsoDateParser', ...)` with a `setUp` that creates a fresh parser; 2. nested groups per behaviour, such as `group('calendar dates', ...)` and `group('rejects', ...)`, so failures read `IsoDateParser rejects month 13`; 3. a `setUpAll` only for something expensive and read-only, such as loading a large table of time-zone rules from disk; 4. `addTearDown` inside the few tests that create temporary files. Keeping hooks close to the tests that need them means a reader sees the whole context of a failing test within one screen, and `dart test -n "IsoDateParser rejects"` runs exactly one behaviour. ## Where package:test stops The framework gives structure, assertions and a runner. Choosing what a unit is, how to name behaviour, and how to arrange a test body are general testing questions; in Flutter projects, `testWidgets` adds a widget tester on top of the same `group`, `setUp` and `expect`.

  • When is setUpAll the right choice despite the warning in package:test's docs?
    When creating the fixture is genuinely expensive and tests only read it: starting a local server, compiling a large asset, opening a read-only database. If any test mutates the fixture, move to `setUp` or reset the mutated part per test, otherwise test order starts to matter.
  • How does addTearDown differ from tearDown?
    `tearDown` is declared at group level and runs after every test in that group. `addTearDown` is called during one running test, cleans up only for it, and runs before the group's `tearDown` callbacks, most recent first. It suits helpers that create resources on demand.

saying these in an interview costs you the question

  • Believes tearDown is skipped when the test body throws
  • Thinks inner-group setUp runs before the outer group's setUp
  • Uses setUpAll for mutable fixtures and wonders why order matters
  • Calls addTearDown at the top level of main
  • Thinks group only changes indentation, not hook scope
open as a page

In Dart's package:test, how do you assert that a call throws or that a Future completes or fails, and when do you need expectLater?

level: middleimportance: must knowfreq 50%

basics

~10 s

Wrap a throwing call in a closure: expect(() => parse('x'), throwsFormatException). For Futures use completion(matcher) or throwsA(isA<T>()); expect returns before those async matches finish, so await expectLater when later code depends on the result.

open as a page

Which dart test flags and package:test annotations control which tests run, on which platform, how many at once and how long each may take?

level: middleimportance: should knowfreq 32%

basics

~20 s

dart test -n filters by name regex, -t/-x by @Tags, -p picks platforms such as vm or chrome, and -j sets concurrent suites. skip: and @Skip disable tests; Timeout and @Timeout change the 30-second default, which dart_test.yaml can set package-wide.

open as a page

In Dart's package:test, how do you assert what a Stream emits, including an error event and the done event?

level: middleimportance: should knowfreq 38%

basics

~20 s

Use stream matchers: expect(stream, emitsInOrder([a, b, emitsDone])) checks a sequence and the end; emits(x) matches one event, emitsError(m) an error event, and emitsThrough skips ahead. Wrap a StreamQueue and await expectLater to check events step by step.

open as a page

A Dart date library's retry-with-timeout and 'in 5 minutes' parsing tests wait real seconds and flake; how does fake_async make them fast and deterministic?

level: seniorimportance: should knowfreq 22%

basics

~20 s

fakeAsync runs the test in a zone whose timers and microtasks fire only when you call elapse or flushMicrotasks, so a 30-second timeout takes no real time; reading time through package:clock's clock.now() lets it fake the current time too.

open as a page