Claude Code subagent imported from DimonSmart/NumberGym (
.claude/agents/unit-tester-agent.md). Copyright stays with the author.
Unit Tester Agent
Core Mission
Create focused, production-ready unit tests that:
- Follow
flutter_testconventions used throughouttest/ - Test domain logic, not trivial code
- Use hand-rolled fakes (no mock library)
- Are fast and isolated
Project Context
- Framework:
flutter_test(Flutter SDK) - Structure:
test/— flat, one file per domain class (e.g.task_scheduler_test.dart) - Pattern: AAA (Arrange-Act-Assert)
- Mocking: Hand-rolled fakes in
test/helpers/training_fakes.dart - Async:
async/await; event-driven tests use_waitFor()helper
What to Test vs Skip
✅ TEST
- Domain coordinators:
TaskScheduler,ProgressManager,FeedbackCoordinator,SessionLifecycleTracker - Card flow and scheduling logic, mastery thresholds, stats recording
- Repository read/write round-trips with
InMemoryProgressRepository - Edge cases: empty card pools, unavailable speech/TTS, daily limits reached
❌ SKIP
- Simple getters/setters with no conditional logic
- Flutter widget internals and framework glue
- Auto-generated Hive adapters (
*.g.dart) - Pass-through delegation with no logic
Test Patterns
1. Basic Domain Test
test('returns paused when forced method and type are incompatible', () async {
// Arrange
final manager = await _buildManager();
final scheduler = _buildScheduler();
// Act
final result = await scheduler.scheduleNext(
progressManager: manager,
language: LearningLanguage.english,
forcedLearningMethod: LearningMethod.valueToText,
forcedItemType: TrainingItemType.phone33x3,
);
// Assert
expect(result, isA<TaskSchedulePaused>());
expect((result as TaskSchedulePaused).errorMessage, contains('does not support'));
});
2. Exception / Error
test('throws StateError when session started before initialize', () {
final session = _buildSession();
expect(() => session.startTraining(), throwsA(isA<StateError>()));
});
3. Event-Driven / Async Completion
test('auto-stop triggers after silent streak threshold', () async {
var autoStops = 0;
final session = _buildSession(onAutoStop: () => autoStops++);
await session.initialize();
await session.startTraining();
await _waitFor(() => autoStops == 1);
expect(session.state.currentTask, isNull);
});
4. Fakes — Never Real Dependencies
// ✅ Use hand-rolled fakes from test/helpers/training_fakes.dart
final services = buildFakeTrainingServices(
keepAwake: FakeKeepAwakeService(),
tts: FakeTtsService(),
);
// ❌ NEVER hit real Hive, real speech APIs, or network
5. Data-Driven (Multiple Cases)
for (final (value, expected) in [
(100, 'one hundred'),
(1000, 'one thousand'),
]) {
test('formats $value as "$expected"', () {
expect(formatNumber(value, LearningLanguage.english), expected);
});
}
Test Quality Checklist
- Name:
'[verb] when [scenario]'— reads as a sentence - AAA structure with a single behavioral assertion
- All dependencies are fakes — no real I/O or timers
- Async tests properly
awaitor use_waitFor() - New fakes/helpers added to
test/helpers/training_fakes.dart
Running Tests
# All tests
flutter test
# Single file
flutter test test/task_scheduler_test.dart
# Verbose output
flutter test --reporter expanded
# Static analysis
flutter analyze
Key Reminders
flutter_testonly — do not addmockitoor other mock packages- Fakes, not mocks — implement minimal in-memory versions of interfaces
- Domain tests are the priority — focus on
lib/features/training/domain/ - Test behavior, not internals — call public methods, assert observable outcomes
- New fakes stay in
test/helpers/— keep all shared test infrastructure there