usecase_forge_test 0.1.0-dev.3 copy "usecase_forge_test: ^0.1.0-dev.3" to clipboard
usecase_forge_test: ^0.1.0-dev.3 copied to clipboard

Deterministic test helpers for the pure Dart usecase_forge runtime.

usecase_forge_test #

Russian translation

English is the primary language of the ecosystem. README.ru.md is a supplementary Russian translation.

Deterministic test helpers for the pure Dart usecase_forge runtime. Tests see the same public snapshots, errors, and terminal history as application code; private queues and runtime behavior remain untouched.

Pre-release: the API may change before 1.0. Pin the version and read the changelog before upgrading.

Installation #

dev_dependencies:
  usecase_forge_test: ^0.1.0-dev.3
dart pub add --dev usecase_forge_test:^0.1.0-dev.3

The application or package under test should declare usecase_forge as a direct dependency when its own code imports core APIs.

package:test is a dependency of this testing package because useCaseTest(...) registers real Dart tests and the field-aware checks return standard Matcher objects. Neither test nor meta is added to the production dependencies of usecase_forge.

Public tools #

Tool Purpose
useCaseTest Creates one UseCase per test, subscribes before act, closes it, and verifies public output
UseCaseTestObserver Records snapshots and stream errors and waits for explicit observable conditions
TestUseCaseClock Advances debounce, throttle, rate-limit timers and timestamps without real delays
Snapshot matchers Check only the State and lifecycle fields named by the test
Execution-entry matchers Check exact command type, phase, result, reason, timing, and other selected fields
UseCaseTestStreamError Keeps an observed stream error together with its original stack trace

A normal lifecycle test #

import 'package:test/test.dart';
import 'package:usecase_forge/usecase_forge.dart';
import 'package:usecase_forge_test/usecase_forge_test.dart';

useCaseTest<CounterUseCase, CounterState>(
  'increment publishes a value and completes',
  build: CounterUseCase.new,
  act: (useCase) => useCase.add(const IncrementCommand()),
  waitFor: (useCase, observer) => observer.waitForHistory(hasLength(1)),
  expectedSnapshots: () => <Object?>[
    isInitialUseCaseSnapshot<CounterState>(executionId: isNotNull),
    isProcessingUseCaseSnapshot<CounterState>(),
    isProcessingUseCaseSnapshot<CounterState>(
      state: const CounterState(1),
    ),
    isFinishedUseCaseSnapshot<CounterState>(
      state: const CounterState(1),
      result: UseCaseExecutionResult.completed,
    ),
  ],
  expectedHistory: () => <Object?>[
    isTerminalUseCaseExecutionEntry<IncrementCommand>(
      result: UseCaseExecutionResult.completed,
    ),
  ],
);

waitFor names the event that ends the scenario. Here the test continues after one terminal history entry appears, without sleeping or inferring how many commands act submitted.

By default, expectedSnapshots excludes only the replayed root snapshot that exists before act. An initial snapshot created for a submitted command is not hidden. Set includeInitialSnapshot: true to check the root snapshot too.

Manual observer for a complex scenario #

test('two commands finish in controlled order', () async {
  final useCase = OrdersUseCase();
  final observer = UseCaseTestObserver<OrderState>(useCase);
  addTearDown(() async {
    await useCase.close();
    await observer.cancel();
  });

  useCase
    ..add(const LoadOrder('a'))
    ..add(const LoadOrder('b'));

  await observer.waitForHistory(hasLength(2));

  expect(
    useCase.history,
    everyElement(
      isTerminalUseCaseExecutionEntry<LoadOrder>(
        result: UseCaseExecutionResult.completed,
      ),
    ),
  );
});

UseCaseTestObserver provides read-only snapshots and streamErrors, plus event-driven waitForSnapshot, waitForHistory, waitForStreamError and waitForDone. Calling cancel() stops only the observer subscription; it does not close the UseCase.

Controlled time #

final clock = TestUseCaseClock(DateTime.utc(2030));
final useCase = SearchUseCase(clock: clock);
final observer = UseCaseTestObserver<SearchState>(useCase);

useCase.add(const SearchCommand('dart'));
clock.advance(const Duration(milliseconds: 300));
await observer.waitForHistory(hasLength(1));

Pass the clock to super(initialState: ..., clock: clock) in the UseCase. advance runs due debounce, throttle and rate-limit callbacks immediately and also controls createdAt, queuedAt, startedAt and finishedAt. No real timer or machine-speed assumption is involved.

Limits of the public test API #

  • A command rejected before terminal history cannot be discovered through the current generic public stream/history API. Test a custom rejection hook or fixture directly. Use the diagnostics channel when rejection events must be observed.
  • AdmissionQueue, PendingQueue and ProcessingRegistry are not exposed. Their internal invariants remain core-package tests, not consumer API.
  • A stream error is not the same as a synchronous exception thrown by useCase.add. Use throwsA for the synchronous call; use waitForStreamError for the stream channel.
  • Repository mocking is outside this package. Pass an ordinary fake to the UseCase constructor and inspect it in verify.

Documentation and project #

This package is created and maintained by Petr Orlov and published through the verified ArkTelos publisher. For package and ecosystem questions, contact packages@arktelos.dev or use the protected ArkTelos contact form.

License #

Licensed under the Apache License, Version 2.0. Copyright 2026 Orlov Petr Petrovich. See LICENSE and NOTICE. The test, meta, and other dependencies retain their own licenses.

0
likes
160
points
167
downloads

Documentation

Documentation
API reference

Publisher

verified publisherarktelos.dev

Weekly Downloads

Deterministic test helpers for the pure Dart usecase_forge runtime.

Homepage
Repository (GitLab)
View/report issues

Topics

#architecture #state-management #testing

License

Apache-2.0 (license)

Dependencies

meta, test, usecase_forge

More

Packages that depend on usecase_forge_test