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.0Flutter projects get flutter_test which re-exports test — no separate install needed.
# Run all tests
dart test
# Flutter
flutter testBasic 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))); // negationType 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 formatNumeric
expect(n, greaterThan(0));
expect(n, lessThanOrEqualTo(100));
expect(n, inInclusiveRange(1, 10));
expect(n, closeTo(3.14, 0.01)); // floating pointComposing 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 integrationConfigure tag behavior in dart_test.yaml:
# dart_test.yaml
tags:
slow:
timeout: 2x # double the timeout for slow tests
integration:
timeout: 60sConfiguring 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.infoTest 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 dataTest 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.