Testing in Dart

The test package, test/group/expect, matchers, and testing asynchronous Dart code.

The test package

Dart's standard testing tool is the test package, maintained by the Dart team but distributed as an ordinary pub.dev package rather than baked directly into the language — it's added as a dev dependency, since tests are never part of a shipped app or package:

YAML
# pubspec.yaml
dev_dependencies:
  test: ^1.25.0
Bash
dart pub get
dart test

By convention, test files live under a top-level test/ directory and end in _test.dartdart test discovers and runs every file matching that pattern automatically.

test(), group(), and expect()

  • test(description, body) defines one individual test case.
  • group(description, body) groups related tests together, purely for organization and readable output — groups can nest.
  • expect(actual, matcher) is the assertion function; a plain value as the second argument is shorthand for an equality check.
Dart
// lib/calculator.dart
class Calculator {
  int add(int a, int b) => a + b;

  double divide(int a, int b) {
    if (b == 0) {
      throw ArgumentError('cannot divide by zero');
    }
    return a / b;
  }
}
Dart
// test/calculator_test.dart
import 'package:test/test.dart';
import 'package:my_app/calculator.dart';

void main() {
  late Calculator calculator;

  setUp(() {
    calculator = Calculator();
  });

  group('Calculator', () {
    test('adds two numbers', () {
      expect(calculator.add(2, 3), equals(5));
    });

    group('divide', () {
      test('returns the correct quotient', () {
        expect(calculator.divide(10, 4), 2.5);
      });

      test('throws ArgumentError when dividing by zero', () {
        expect(() => calculator.divide(10, 0), throwsArgumentError);
      });
    });
  });
}
Bash
dart test
Plaintext
00:00 +0: Calculator adds two numbers
00:00 +1: Calculator divide returns the correct quotient
00:00 +2: Calculator divide throws ArgumentError when dividing by zero
00:00 +3: All tests passed!

setUp(() { ... }) runs before every single test() in its scope (not once per file) — exactly the same "fresh state per test" guarantee that before(:each) provides in RSpec or setUp() provides in XCTest, and for the same reason: it stops one test's leftover state from silently affecting the next one.

Why throwsArgumentError needs a closure

expect(() => calculator.divide(10, 0), throwsArgumentError) passes a function (() => calculator.divide(10, 0)), not the result of calling calculator.divide(10, 0) directly. That distinction matters: if the divide call were evaluated eagerly as an argument to expect, the ArgumentError would be thrown immediately, before expect ever got a chance to catch and check it — crashing the test with an uncaught exception rather than reporting a clean, ordinary test failure or pass. Wrapping it in () => ... hands expect the means to invoke the code itself, inside its own error-catching logic.

Matchers

Matcher Checks
equals(x) (or a bare value) Deep equality — the default when you pass a plain value directly
throwsA(isA<SomeError>()) The wrapped call throws a specific exception type
throwsArgumentError, throwsRangeError, etc. Shorthand for common built-in exception types
isNull / isNotNull The value is (or isn't) null
contains(x) A String, List, or Map contains the given element/substring/key
isA<T>() The value is an instance of type T

Testing asynchronous code

A test body can be async, and expect works the same way once the value being checked has been awaited:

Dart
Future<String> fetchUserName(int id) async {
  await Future.delayed(const Duration(milliseconds: 100));
  return 'Ali';
}

test('fetchUserName returns the expected name', () async {
  final name = await fetchUserName(1);
  expect(name, equals('Ali'));
});

For code that returns a Stream, the package also provides expectLater combined with stream matchers like emitsInOrder, for asserting on a whole sequence of emitted values rather than a single awaited result.

Common mistakes

  • Passing the already-evaluated result of a throwing call to expect instead of a closure — expect(calculator.divide(10, 0), throwsArgumentError) throws immediately and crashes the test, rather than letting expect catch and verify the exception; it must be expect(() => calculator.divide(10, 0), throwsArgumentError).
  • Forgetting setUp() runs before every test, not once for the whole file — code that expects state built in one test to still be present in the next will fail unpredictably depending on test order.
  • Not marking (or not awaiting inside) an async test body, so the test function returns before the real asynchronous work — and its expect call — has actually run, letting a genuinely broken assertion go unnoticed.
  • Writing one enormous test() that checks many unrelated behaviors at once instead of several small, clearly-named tests — when it fails, the output doesn't tell you which specific behavior actually broke.

Interview questions

Q: Why must a call that's expected to throw be wrapped in a closure when used with expect and a matcher like throwsArgumentError? Because expect's matcher needs to be the one that actually invokes the code, inside its own try/catch-style logic, in order to catch the exception and check it against the matcher. If the throwing call were evaluated directly as an argument to expect, the exception would already have been thrown — and gone uncaught — before expect ever ran, crashing the test instead of reporting a clean pass/fail.

Q: What's the difference between test() and group() in Dart's test package? test() defines one individual test case with its own assertions. group() is purely organizational — it nests related tests together under a shared description for more readable output, and groups can be nested inside other groups, but a group() itself contains no assertions of its own.

Q: How do you test asynchronous Dart code with the test package? Mark the test's callback function async and await the asynchronous call inside it exactly like any other async Dart code, then run expect on the resolved value. Because the test runner awaits the test function's returned Future before reporting a result, an assertion after an await is still correctly checked before the test is marked as passed or failed.