Dart test Package: Unit Tests, Groups, setUp, Matchers, and Test Utilities

Dart test Package: Unit Tests, Groups, setUp, Matchers, and Test Utilities

The test package is Dart's standard testing library — used by both pure Dart projects and Flutter apps. It provides test(), group(), setUp(), tearDown(), async test support, stream matchers, and a rich set of built-in matchers. Everything in Flutter's flutter_test is built on top of it.

Key Takeaways

group() is for organization, not isolation. Each test still runs in the same isolate. Use setUp() and tearDown() to reset state between tests in a group.

Async tests need async/await or expectAsync. A test that returns void without awaiting futures will pass even if assertions fail asynchronously.

Matchers compose. expect(value, allOf([isNotNull, isA<String>(), contains('foo')])) — chain matchers with allOf, anyOf, and isNot.

throwsA is the right way to test exceptions. Use expect(() => fn(), throwsA(isA<ArgumentError>())) — not try/catch.

Tags let you slice your test suite. @Tags(['slow']) on a file or tags: 'integration' on a test, then run with dart test --tags slow.

Installing the test Package

For pure Dart projects:

# pubspec.yaml
dev_dependencies:
  test: ^1.25.0

Flutter projects get flutter_test which re-exports test — no separate install needed.

# Run all tests
dart test

# Flutter
flutter test

Basic Test Structure

import 'package:test/test.dart';

void main() {
  test('adds two numbers', () {
    expect(2 + 2, equals(4));
  });

  test('string contains substring', () {
    expect('Hello, World!', contains('World'));
  });
}

test() takes a description and a callback. The callback is your test body.

Grouping Tests

Use group() to organize related tests:

void main() {
  group('Calculator', () {
    test('adds', () => expect(Calculator.add(2, 3), 5));
    test('subtracts', () => expect(Calculator.subtract(5, 3), 2));
    test('throws on division by zero', () {
      expect(() => Calculator.divide(10, 0), throwsA(isA<ArgumentError>()));
    });
  });

  group('String utilities', () {
    test('trims whitespace', () => expect('  hi  '.trim(), 'hi'));
    test('splits on comma', () => expect('a,b,c'.split(',').length, 3));
  });
}

Test output shows nested names: Calculator adds, Calculator subtracts.

setUp and tearDown

Run code before/after each test:

void main() {
  late Database db;

  setUp(() {
    db = Database.inMemory();
    db.insert('users', {'id': 1, 'name': 'Alice'});
  });

  tearDown(() {
    db.close();
  });

  test('finds user by id', () {
    final user = db.find('users', id: 1);
    expect(user['name'], 'Alice');
  });

  test('returns null for missing id', () {
    final user = db.find('users', id: 99);
    expect(user, isNull);
  });
}

setUpAll() and tearDownAll() run once per group, not per test — use carefully, as shared state can leak between tests.

Built-In Matchers

Equality and Identity

expect(value, equals(42));         // == comparison
expect(value, same(instance));     // identical() — same object reference
expect(value, isNot(equals(0)));   // negation

Type Checking

expect(value, isA<String>());
expect(value, isNull);
expect(value, isNotNull);
expect(value, isTrue);
expect(value, isFalse);

Collections

expect(list, isEmpty);
expect(list, isNotEmpty);
expect(list, hasLength(3));
expect(list, contains('foo'));
expect(list, containsAll(['a', 'b']));
expect(list, orderedEquals(['a', 'b', 'c']));
expect(list, unorderedEquals(['c', 'a', 'b']));
expect(map, containsPair('key', 'value'));

Strings

expect(str, startsWith('Hello'));
expect(str, endsWith('World'));
expect(str, contains('ello'));
expect(str, matches(RegExp(r'^\d{4}-\d{2}-\d{2}$'))); // date format

Numeric

expect(n, greaterThan(0));
expect(n, lessThanOrEqualTo(100));
expect(n, inInclusiveRange(1, 10));
expect(n, closeTo(3.14, 0.01)); // floating point

Composing Matchers

expect(
  value,
  allOf([
    isA<String>(),
    isNotEmpty,
    startsWith('prefix'),
  ]),
);

expect(value, anyOf([equals('a'), equals('b')]));

Exception Testing

// Throws any exception
expect(() => dangerousCall(), throwsException);

// Throws specific type
expect(() => parseDate('not-a-date'), throwsA(isA<FormatException>()));

// Throws with specific message
expect(
  () => parseDate('not-a-date'),
  throwsA(
    isA<FormatException>().having(
      (e) => e.message,
      'message',
      contains('Invalid date'),
    ),
  ),
);

// Throws ArgumentError specifically
expect(() => divide(10, 0), throwsArgumentError);

Async Tests

Futures

test('async operation completes', () async {
  final result = await fetchData();
  expect(result, isNotEmpty);
});

// Or using Future directly
test('future resolves to expected value', () {
  expect(fetchUser(id: 1), completion(isA<User>()));
});

// Async exception
test('future throws on invalid input', () {
  expect(fetchUser(id: -1), throwsA(isA<ArgumentError>()));
});

Streams

test('stream emits expected values', () async {
  final stream = numberStream(); // emits 1, 2, 3

  expect(
    stream,
    emitsInOrder([1, 2, 3, emitsDone]),
  );
});

test('stream emits then errors', () {
  final stream = failingStream();

  expect(
    stream,
    emitsInOrder([
      emits(1),
      emitsError(isA<NetworkException>()),
    ]),
  );
});

test('stream emits at least one value', () {
  expect(sensorStream(), emitsThrough(greaterThan(100)));
});

Stream matchers: emits(), emitsInOrder(), emitsError(), emitsDone, emitsThrough(), neverEmits(), mayEmit().

expectAsync

Use expectAsync when callbacks are called asynchronously and you need the test to wait:

test('callback is invoked', () {
  final onData = expectAsync1((String data) {
    expect(data, 'hello');
  });

  fetchData(callback: onData);
});

// Expect multiple calls
test('callback called 3 times', () {
  final onEvent = expectAsync0(() {}, count: 3);
  eventEmitter.listen((_) => onEvent());
});

Tags

Tag tests to run subsets:

@Tags(['slow', 'integration'])
import 'package:test/test.dart';

void main() {
  test('database roundtrip', () { ... }, tags: 'db');
  test('api call', () { ... }, tags: ['slow', 'network']);
}

Run by tag:

dart test --tags slow
dart test --exclude-tags integration

Configure tag behavior in dart_test.yaml:

# dart_test.yaml
tags:
  slow:
    timeout: 2x  # double the timeout for slow tests
  integration:
    timeout: 60s

Configuring Tests

# dart_test.yaml
timeout: 30s
concurrency: 4
platforms: [vm, chrome]

test_on: vm  # restrict all tests to VM platform

tags:
  golden:
    platforms: [vm]

Custom Matchers

Matcher hasPositiveId() => predicate(
  (obj) => obj is User && obj.id > 0,
  'has positive id',
);

// Usage
expect(user, hasPositiveId());

For complex matchers, extend Matcher:

class _IsValidEmail extends Matcher {
  const _IsValidEmail();

  @override
  bool matches(Object? item, Map matchState) {
    if (item is! String) return false;
    return RegExp(r'^[\w.]+@[\w.]+\.\w+$').hasMatch(item);
  }

  @override
  Description describe(Description description) =>
      description.add('a valid email address');
}

const isValidEmail = _IsValidEmail();

// Usage
expect(user.email, isValidEmail);

Timeout Control

test('slow operation', () async {
  await longRunningProcess();
}, timeout: const Timeout(Duration(minutes: 2)));

// Disable timeout
test('unlimited', () async {
  await reallyLongProcess();
}, timeout: Timeout.none);

Skipping Tests

test('not ready yet', () { ... }, skip: true);
test('platform specific', () { ... }, skip: !Platform.isLinux);

group('deprecated API', () {
  // All tests in this group skipped
}, skip: 'API removed in v2');

Running Tests

# All tests
dart test

# Specific file
dart test test/calculator_test.dart

# By name pattern
dart test --name "adds"

# By tag
dart test --tags integration

# With coverage (requires coverage package)
dart test --coverage=coverage/
dart pub global run coverage:format_coverage --lcov --in=coverage/ --out=lcov.info

Test File Conventions

project/
  lib/
    calculator.dart
    string_utils.dart
  test/
    calculator_test.dart      # mirrors lib structure
    string_utils_test.dart
    helpers/
      test_doubles.dart       # shared fakes/mocks
      fixtures.dart           # test data

Test files must end with _test.dart to be discovered by dart test.

Differences: dart test vs flutter_test

Feature dart test flutter_test
Widget testing No Yes (testWidgets, WidgetTester)
pump() / pumpAndSettle() No Yes
Golden image testing No Yes
find.* finders No Yes
Async/stream testing Yes Yes
Custom matchers Yes Yes
Platform VM, browser VM

Flutter's flutter_test imports test internally — everything in this guide applies to Flutter tests too.

Read more

Start now free