dev_test: solo/skip on top of package:test
package:dev_test/test.dart exports the same API as package:test/test.dart
(test, group, setUp, tearDown, setUpAll, tearDownAll, expect,
all matchers and annotations) and adds solo_test, solo_group,
skip_test, skip_group and testDescriptions. Tests still run with
dart test / flutter test; nothing else changes. Works on the VM, in the
browser and on node.
import 'package:dev_test/test.dart';
void main() {
group('group', () {
test('test', () {
expect(true, isTrue);
expect(testDescriptions, ['group', 'test']);
});
});
}
Guidelines
Setup
- Add both to
dev_dependencies: test (the runner) and dev_test.
- In
test/*_test.dart replace import 'package:test/test.dart'; with
import 'package:dev_test/test.dart';. Never import both in one file:
test, group, expect... are defined by each and conflict.
package:dev_test/dev_test.dart is the same library under another name.
dev_test provides the dart test API only. It does not provide
testWidgets/WidgetTester; keep package:flutter_test for widget
tests.
Re-exported package:test API
- Structure:
test, group, setUp, tearDown, setUpAll,
tearDownAll, addTearDown, printOnFailure, markTestSkipped,
pumpEventQueue, registerException, spawnHybridUri,
spawnHybridCode.
- Annotations and parameters:
TestOn, Timeout, Skip, Tags,
OnPlatform, Retry; test(..., testOn: 'vm', timeout: const Timeout(Duration(minutes: 2)), skip: 'reason', onPlatform: {...})
behave exactly as in package:test.
- Expectations:
expect, expectLater, expectAsync0..6,
expectAsyncUntil0..6, fail, TestFailure, neverCalled, prints,
throwsA, throwsArgumentError..., completes, completion,
emits, emitsInOrder, StreamMatcher, and every package:matcher
matcher (equals, isTrue, isNull, contains, hasLength,
isA<T>(), closeTo, ...).
solo and skip
solo_test('name', body) / solo_group('name', body): only the
solo-marked tests and groups of that file run; the others are reported
as skipped. Use it from the IDE to iterate on one test without
dart test -n. test(..., solo: true) is the parameter form.
skip_test('name', body) / skip_group('name', body): temporarily skip
without touching the body. For a permanent skip use the skip:
parameter (test('x', body, skip: 'flaky on node')) which is not
deprecated.
- The four functions are
@Deprecated('Dev only'): the analyzer reports
each use, and a CI running dart analyze --fatal-infos (what
dev_build's run_ci does) fails. That is intentional: remove them
before committing. Do not silence them with // ignore in committed
code.
- They accept the same named parameters as
test/group (testOn,
timeout, skip, onPlatform).
testDescriptions
testDescriptions is a List<String> naming the enclosing groups and
the current test. Inside a test body it is [...groups, test]. Inside
setUp, tearDown, setUpAll, tearDownAll and while a group body is
being declared it is the group path only ([...groups]), the test name
is not known there. Async tests that run concurrently each see their
own value.
- Use it to derive unique resources per test (a database name, a temp
folder, a document path) instead of repeating the test name:
testDescriptions.join('_') from the test body.
- Outside any group or test it is
[].
Interactive menu runner
import 'package:dev_test/dev_test_menu.dart' (exports test.dart too)
and wrap the declarations in mainDevTestMenu(() { ... }, arguments: args): each group becomes a menu, each test an item, setUp/tearDown
run around each item, setUpAll/tearDownAll on menu enter/leave, and
expect failures are printed instead of aborting. Run it with
dart run test/my_menu.dart (name the file so dart test ignores it,
or keep it in example//tool/). See the dev-build-menu skill for
the console commands (0, 1..., ., ?).
Examples
Isolate one test while debugging
import 'package:dev_test/test.dart';
void main() {
test('other', () {
fail('not run while a solo test exists');
});
// Deprecated on purpose: remove before committing.
solo_test('the one', () {
expect(1 + 1, 2);
});
}
Skip a group temporarily
import 'package:dev_test/test.dart';
void main() {
skip_group('slow', () {
test('a', () {});
test('b', () {});
});
test('fast', () {
expect(true, isTrue);
});
}
Per-test resources from testDescriptions
import 'package:dev_test/test.dart';
String dbName() => '${testDescriptions.join('_')}.db';
void main() {
group('store', () {
setUp(() {
// Group path only here.
expect(testDescriptions, ['store']);
});
test('insert', () {
expect(dbName(), 'store_insert.db');
});
group('nested', () {
test('read', () {
expect(testDescriptions, ['store', 'nested', 'read']);
expect(dbName(), 'store_nested_read.db');
});
});
});
}
Platform and timeout annotations still work
@TestOn('vm')
library;
import 'package:dev_test/test.dart';
void main() {
test('io', () async {
await Future<void>.delayed(const Duration(milliseconds: 10));
}, timeout: const Timeout(Duration(minutes: 2)));
test('web only', () {}, testOn: 'browser', skip: 'no browser in CI');
}
Async matchers
import 'dart:async';
import 'package:dev_test/test.dart';
void main() {
test('futures and streams', () async {
await expectLater(Future.value(1), completion(1));
expect(() => throw ArgumentError('x'), throwsArgumentError);
var controller = StreamController<int>();
controller
..add(1)
..add(2)
..close();
await expectLater(controller.stream, emitsInOrder([1, 2, emitsDone]));
});
}
Run a suite as a console menu
// example/main_menu.dart — dart run example/main_menu.dart
import 'package:dev_test/dev_test_menu.dart';
void main(List<String> args) {
mainDevTestMenu(() {
group('group', () {
setUp(() => print('before each'));
test('passes', () {
expect(testDescriptions, ['group', 'passes']);
});
test('fails', () {
expect(true, isFalse); // printed, does not stop the menu
});
});
}, arguments: args);
}
Common mistakes
- Importing
package:test/test.dart and package:dev_test/test.dart in
the same file (ambiguous test, group, expect).
- Committing
solo_test/skip_test: CI analysis fails on the deprecation
and, with solo_*, most tests silently stop running.
- Reading
testDescriptions in a top-level variable initializer (it is
[]), or expecting the test name inside setUp (only the groups are
known there).
- Expecting
mainDevTestMenu files to run under dart test: they are
scripts started with dart run.
1---2name: dev-test-testing3description: Use when writing or running Dart tests with package:dev_test, a drop-in replacement for import 'package:test/test.dart': solo_test, solo_group, skip_test, skip_group to isolate or skip one test from the IDE, testDescriptions for the current group/test path, mainDevTestMenu to run a test file as an interactive console menu, and which package:test API (test, group, setUp, tearDown, expect, matchers, TestOn, Timeout, Skip, addTearDown, printOnFailure) it re-exports.4---56# dev_test: solo/skip on top of package:test78`package:dev_test/test.dart` exports the same API as `package:test/test.dart`9(`test`, `group`, `setUp`, `tearDown`, `setUpAll`, `tearDownAll`, `expect`,10all matchers and annotations) and adds `solo_test`, `solo_group`,11`skip_test`, `skip_group` and `testDescriptions`. Tests still run with12`dart test` / `flutter test`; nothing else changes. Works on the VM, in the13browser and on node.1415```dart16import 'package:dev_test/test.dart';1718void main() {19 group('group', () {20 test('test', () {21 expect(true, isTrue);22 expect(testDescriptions, ['group', 'test']);23 });24 });25}26```2728## Guidelines2930### Setup3132* Add both to `dev_dependencies`: `test` (the runner) and `dev_test`.33* In `test/*_test.dart` replace `import 'package:test/test.dart';` with34 `import 'package:dev_test/test.dart';`. Never import both in one file:35 `test`, `group`, `expect`... are defined by each and conflict.36 `package:dev_test/dev_test.dart` is the same library under another name.37* `dev_test` provides the `dart test` API only. It does not provide38 `testWidgets`/`WidgetTester`; keep `package:flutter_test` for widget39 tests.4041### Re-exported package:test API4243* Structure: `test`, `group`, `setUp`, `tearDown`, `setUpAll`,44 `tearDownAll`, `addTearDown`, `printOnFailure`, `markTestSkipped`,45 `pumpEventQueue`, `registerException`, `spawnHybridUri`,46 `spawnHybridCode`.47* Annotations and parameters: `TestOn`, `Timeout`, `Skip`, `Tags`,48 `OnPlatform`, `Retry`; `test(..., testOn: 'vm', timeout:49 const Timeout(Duration(minutes: 2)), skip: 'reason', onPlatform: {...})`50 behave exactly as in `package:test`.51* Expectations: `expect`, `expectLater`, `expectAsync0..6`,52 `expectAsyncUntil0..6`, `fail`, `TestFailure`, `neverCalled`, `prints`,53 `throwsA`, `throwsArgumentError`..., `completes`, `completion`,54 `emits`, `emitsInOrder`, `StreamMatcher`, and every `package:matcher`55 matcher (`equals`, `isTrue`, `isNull`, `contains`, `hasLength`,56 `isA<T>()`, `closeTo`, ...).5758### solo and skip5960* `solo_test('name', body)` / `solo_group('name', body)`: only the61 solo-marked tests and groups of that file run; the others are reported62 as skipped. Use it from the IDE to iterate on one test without63 `dart test -n`. `test(..., solo: true)` is the parameter form.64* `skip_test('name', body)` / `skip_group('name', body)`: temporarily skip65 without touching the body. For a permanent skip use the `skip:`66 parameter (`test('x', body, skip: 'flaky on node')`) which is not67 deprecated.68* The four functions are `@Deprecated('Dev only')`: the analyzer reports69 each use, and a CI running `dart analyze --fatal-infos` (what70 `dev_build`'s `run_ci` does) fails. That is intentional: remove them71 before committing. Do not silence them with `// ignore` in committed72 code.73* They accept the same named parameters as `test`/`group` (`testOn`,74 `timeout`, `skip`, `onPlatform`).7576### testDescriptions7778* `testDescriptions` is a `List<String>` naming the enclosing groups and79 the current test. Inside a test body it is `[...groups, test]`. Inside80 `setUp`, `tearDown`, `setUpAll`, `tearDownAll` and while a group body is81 being declared it is the group path only (`[...groups]`), the test name82 is not known there. Async tests that run concurrently each see their83 own value.84* Use it to derive unique resources per test (a database name, a temp85 folder, a document path) instead of repeating the test name:86 `testDescriptions.join('_')` from the test body.87* Outside any group or test it is `[]`.8889### Interactive menu runner9091* `import 'package:dev_test/dev_test_menu.dart'` (exports `test.dart` too)92 and wrap the declarations in `mainDevTestMenu(() { ... }, arguments:93 args)`: each group becomes a menu, each test an item, `setUp`/`tearDown`94 run around each item, `setUpAll`/`tearDownAll` on menu enter/leave, and95 `expect` failures are printed instead of aborting. Run it with96 `dart run test/my_menu.dart` (name the file so `dart test` ignores it,97 or keep it in `example/`/`tool/`). See the `dev-build-menu` skill for98 the console commands (`0`, `1`..., `.`, `?`).99100## Examples101102### Isolate one test while debugging103104```dart105import 'package:dev_test/test.dart';106107void main() {108 test('other', () {109 fail('not run while a solo test exists');110 });111 // Deprecated on purpose: remove before committing.112 solo_test('the one', () {113 expect(1 + 1, 2);114 });115}116```117118### Skip a group temporarily119120```dart121import 'package:dev_test/test.dart';122123void main() {124 skip_group('slow', () {125 test('a', () {});126 test('b', () {});127 });128 test('fast', () {129 expect(true, isTrue);130 });131}132```133134### Per-test resources from testDescriptions135136```dart137import 'package:dev_test/test.dart';138139String dbName() => '${testDescriptions.join('_')}.db';140141void main() {142 group('store', () {143 setUp(() {144 // Group path only here.145 expect(testDescriptions, ['store']);146 });147 test('insert', () {148 expect(dbName(), 'store_insert.db');149 });150 group('nested', () {151 test('read', () {152 expect(testDescriptions, ['store', 'nested', 'read']);153 expect(dbName(), 'store_nested_read.db');154 });155 });156 });157}158```159160### Platform and timeout annotations still work161162```dart163@TestOn('vm')164library;165166import 'package:dev_test/test.dart';167168void main() {169 test('io', () async {170 await Future<void>.delayed(const Duration(milliseconds: 10));171 }, timeout: const Timeout(Duration(minutes: 2)));172173 test('web only', () {}, testOn: 'browser', skip: 'no browser in CI');174}175```176177### Async matchers178179```dart180import 'dart:async';181182import 'package:dev_test/test.dart';183184void main() {185 test('futures and streams', () async {186 await expectLater(Future.value(1), completion(1));187 expect(() => throw ArgumentError('x'), throwsArgumentError);188 var controller = StreamController<int>();189 controller190 ..add(1)191 ..add(2)192 ..close();193 await expectLater(controller.stream, emitsInOrder([1, 2, emitsDone]));194 });195}196```197198### Run a suite as a console menu199200```dart201// example/main_menu.dart — dart run example/main_menu.dart202import 'package:dev_test/dev_test_menu.dart';203204void main(List<String> args) {205 mainDevTestMenu(() {206 group('group', () {207 setUp(() => print('before each'));208 test('passes', () {209 expect(testDescriptions, ['group', 'passes']);210 });211 test('fails', () {212 expect(true, isFalse); // printed, does not stop the menu213 });214 });215 }, arguments: args);216}217```218219## Common mistakes220221* Importing `package:test/test.dart` and `package:dev_test/test.dart` in222 the same file (ambiguous `test`, `group`, `expect`).223* Committing `solo_test`/`skip_test`: CI analysis fails on the deprecation224 and, with `solo_*`, most tests silently stop running.225* Reading `testDescriptions` in a top-level variable initializer (it is226 `[]`), or expecting the test name inside `setUp` (only the groups are227 known there).228* Expecting `mainDevTestMenu` files to run under `dart test`: they are229 scripts started with `dart run`.