Dart Testing Ecosystem

repository·master·Indexed 19 days ago

https://github.com/dart-lang/test

A collection of packages for testing Dart and Flutter applications. Includes package:test and package:matcher for core test runners and expectations, fake_async for deterministic asynchronous testing, and the experimental package:checks and package:checks_codegen for subject-based expectation chains and code generation.

Tokens
10.3K
Snippets
26
Records
43
Agent score
67%

What's inside dart-lang-test

  1. Overview of the Dart testing ecosystem

    master

    The dart-lang/test repository contains several specialized packages for testing in Dart and Flutter.

    • package:test: The primary, full-featured library for writing and running tests across different platforms. This is the standard choice for most users.
    • package:checks: A modern framework for expressing test expectations using a literate API. It is intended as a successor to package:matcher.
    • package:matcher: Provides an extensible Matcher class and built-in implementations for common test expectations.
    • package:fake_async: Used for deterministic testing by providing fake asynchronous events like timers and microtasks.
    • package:test_process: A utility for managing test processes, including starting them, validating stdout/stderr, and checking exit codes.
    • package:test_descriptor: An API for defining and verifying file and directory structures.

    Note: package:test_api and package:test_core are considered implementation details and are generally not intended for direct use by end-users.

  2. Overview of package:test_core

    master

    The test_core package provides a minimal foundation for writing and running tests in Dart. It includes the core logic for test execution and provides extension points for developers who need to implement a custom test runner.

    Note: This package is currently not intended for public use, and its API is not yet stable.

  3. Overview of package:checks_codegen

    master

    package:checks_codegen is an experimental companion to package:checks. It is used to generate extensions that allow you to read specific fields from subjects under test during testing.

    Warning: This package is experimental and part of the labs.dart.dev publisher. It is subject to frequent API changes and breaking changes. For production use cases, use package:test and package:matcher instead.

  4. Use TestProcess to test subprocesses

    master

    The test_process package provides the TestProcess class, which wraps dart:io's Process class. It is designed to make testing subprocesses easier by providing line-by-line string access to standard output and error streams via a pull-based StreamQueue API.

    import 'package:test_process/test_process.dart';
    
    // Start a process similar to dart:io
    var process = await TestProcess.start('dart', ['pub', 'get']);
  5. Read standard output line-by-line with TestProcess

    master

    Unlike dart:io which uses binary streams, TestProcess.stdout and TestProcess.stderr emit strings for each line. These are StreamQueues, allowing you to pull lines one by one using the .next getter.

    To access the output without consuming the queues, use stdoutStream() or stderrStream(). These methods return a new stream that replays the output from the beginning.

    import 'package:test/test.dart';
    import 'package:test_process/test_process.dart';
    
    void main() {
      test('pull-based output reading', () async {
        var process = await TestProcess.start('dart', ['pub', 'get']);
    
        // Pull the first line
        var firstLine = await process.stdout.next;
        expect(firstLine, equals('Resolving dependencies...'));
    
        // Loop until a specific line is found
        String next;
        do {
          next = await process.stdout.next;
        } while (next != 'Got dependencies!');
    
        await process.shouldExit(0);
      });
    }
  6. Best Practice: Prefer TypeMatcher over predicate

    master

    While the predicate utility is convenient for testing arbitrary properties, it discards context and produces opaque failure messages.

    Instead, use isA<SomeType>() and the TypeMatcher.having API. This approach allows you to test derived properties in a structured way, ensuring that failure messages are distinguishable and actionable by preserving the context of the object's structure.

  7. Benefits of using package:checks over package:matcher

    master

    Using package:checks provides several advantages over the legacy package:matcher:

    Static Type Safety

    Expectations are statically restricted to types appropriate for the value. For example, check(1).contains(1) will trigger a static error because contains is not defined for an integer, whereas expect(1, contains(1)) would only fail at runtime.

    Improved IDE Experience

    Because of static typing, IDE autocomplete suggestions are narrowed to only those expectations and utilities that are valid for the specific type of the subject.

    Robust Asynchronous Expectations

    package:checks enforces a strict contract for asynchronous expectations. Asynchronous expectations always return a Future, and the framework refuses to use an asynchronous expectation when a synchronous answer is required, preventing false successes or misleading errors common in the legacy implementation.

  8. Compose expectations using cascade syntax and utilities

    master

    You can chain multiple expectations against a single value using Dart's cascade syntax (..). If a chain fails, the output includes descriptions of the expectations that successfully passed before the failure.

    Key Composition Utilities:

    • Cascade (..): For standard sequential expectations.
    • which: Used when a cascade is not possible (e.g., when checking a property like length that returns a non-Subject value).
    • has: Used to extract fields or derived values from an object to perform further checks.
    • Derived Subjects: Some extensions (like .length) return a new Subject for the derived value, allowing further chaining.
    // Using cascade syntax
    check(someString)
      ..startsWith('a')
      ..endsWith('z')
      ..contains('lmno');
    
    // Using `which` for properties that don't return a Subject
    check(someString).length.which((l) => l
      ..isGreaterThan(10)
      ..isLessThan(100));
    
    // Using `has` to extract and check properties
    check(someValue)
      .has((value) => value.property, 'property')
      .equals(expectedPropertyValue);
    
    // Chaining from a derived Subject
    check(someString).length.equals(expectedLength);
  9. Integrate `fake_async` with the `clock` package

    master

    By default, FakeAsync cannot control the time reported by DateTime.now() or the Stopwatch class because they are not part of dart:async. To ensure these classes respect the controlled time in your tests, you must use the clock package.

    If you use clock.now() instead of DateTime.now() and clock.stopwatch() instead of Stopwatch(), FakeAsync will automatically override them to use the same notion of time as dart:async classes.

  10. Best Practice: Prefer semantically meaningful matchers

    master

    When writing tests, prefer matchers that understand the semantics of the object being tested rather than comparing derived values (like .length). Semantically meaningful matchers provide much clearer failure messages that explain why a test failed, rather than just showing a mismatch of primitive values.

    Bad (comparing derived values): expect(someList.length, 1); Failure: Expected: <1>, Actual: <2>

    Good (semantic matcher): expect(someList, hasLength(1)); Failure: Expected: an object with length of <1>, Actual: ['expected value', 'unexpected value'], Which: has length of <2>

  11. Test asynchronous streams with stream matchers

    master

    The test package provides expressive and composable matchers for validating values emitted by a Stream. You can use expectLater() to match against stream events.

    Common patterns include:

    • Matching a sequence of events with emitsInOrder().
    • Matching any one of several possible events with emitsAnyOf().
    • Asserting a stream finishes immediately after a match with emitsDone.

    Stream matchers can also match StreamQueue objects from the async package. Unlike standard Streams, matching against a StreamQueue allows you to consume specific events while leaving the rest of the queue available for further testing.

    import 'dart:async';
    import 'package:test/test.dart';
    
    void main() {
      test('process emits status messages', () {
        var stdoutLines = Stream.fromIterable([
          'Ready.',
          'Loading took 150ms.',
          'Succeeded!'
        ]);
    
        expect(stdoutLines, emitsInOrder([
          'Ready.',
          startsWith('Loading took'),
          emitsAnyOf(['Succeeded!', 'Failed!']),
          emitsDone
        ]));
      });
    }