usecase_forge_test
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. UsethrowsAfor the synchronous call; usewaitForStreamErrorfor 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
- Observer and event-driven waiting
- Public test coverage boundaries
- Runnable examples
- Package documentation
- UseCase Forge product site
- GitLab repository
- Issue tracker
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.
Libraries
- usecase_forge_test
- Deterministic test helpers for UseCase Forge.