| name | tdd |
| description | Test-Driven Development discipline. Use when implementing any feature or bugfix BEFORE writing implementation code. Enforces RED-GREEN-REFACTOR cycle with mandatory failing-test verification. |
Test-Driven Development (TDD)
Adapted from superpowers/skills/test-driven-development. Trimmed for solo Flutter/TS workflow.
Core principle
Write the test first. Watch it fail. Write minimal code to pass.
If you didn't watch the test fail, you don't know if it tests the right thing.
The Iron Law
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
Wrote code before the test? Delete it. Start over from the test.
Exceptions (must ask the user first):
- Throwaway prototype.
- Generated code (codegen, freezed, json_serializable).
- Config files.
"Skip TDD just this once" = rationalization. Stop.
Red-Green-Refactor
1. RED — Write a failing test
// Flutter example - VSmartwatch SOS confirm flow
test('SosController rejects confirm when no active fall event', () async {
final controller = SosController(repo: FakeSosRepository(activeFall: null));
final result = await controller.confirmSOS();
expect(result.isFailure, true);
expect(result.error, isA<NoActiveFallError>());
});
test('returns 400 when imei missing', async () => {
const res = await request(app).post('/api/admin/devices').send({ name: 'watch-1' });
expect(res.status).toBe(400);
expect(res.body.error).toBe('imei required');
});
Requirements:
- One behavior per test.
- A clear name (describes behavior, not implementation).
- Real-ish code where possible (in-memory fakes for repos, mocked dio responses, test Postgres for Express+Prisma — not full mocking of everything).
2. Verify RED — run the test, watch it fail
flutter test test/features/emergency/sos_controller_test.dart
pytest tests/test_sos_service.py::test_rejects_when_no_active_fall
Confirm:
- The test FAILS (not errors out due to a typo).
- The failure message matches what you expect.
Test passes immediately? You're testing existing behaviour. Rewrite the test.
Test errors out? Fix the typo, re-run until it fails for the right reason.
3. GREEN — minimum code to pass
Write only enough code for the test to pass. No extra options, no abstractions, no "for later" code.
class SosController {
SosController({required this.repo});
final SosRepository repo;
Future<Result<SosConfirmation, AppError>> confirmSOS() async {
final activeFall = await repo.getActiveFall();
if (activeFall == null) return Result.failure(NoActiveFallError());
return Result.success(await repo.confirmSOS(fallId: activeFall.id));
}
}
4. Verify GREEN — test passes + nothing else broke
flutter test
- New test passes ✓
- Old tests still pass ✓
- Output is clean (no warnings, no error logs)
5. REFACTOR — clean up while green
- Rename for clarity, extract helpers, remove duplication.
- No new behaviour during refactor.
- Tests stay green after every refactor step.
6. Commit
git add test/features/emergency/ lib/features/emergency/
git commit -m "feat(emergency): tu choi confirmSOS khi khong co fall event active"
7. Loop
Next failing test for the next behaviour.
Bug fix → reproduction test
- Write the test that reproduces the bug first (don't touch the code).
- Run → confirm FAIL with the right symptom.
- Fix the code → run → PASS.
- Revert the fix → run → FAIL (proof the test actually catches the bug).
- Restore the fix → run → PASS.
- Commit.
Skip step 4 → the test might pass for the wrong reason.
When stuck
| Problem | Direction |
|---|
| Don't know how to test something | Write the wished-for API first. "I want this function to look like ..." then test that. |
| Test setup is too complex | Design is too coupled. Inject dependencies, use repository pattern. |
| Need to mock everything | Code coupling is high. Use DI + in-memory fakes instead of mocks. |
| Async timing issues | Use pumpAndSettle(), expectLater(future, completes), polling — never Future.delayed. |
Verification checklist before claiming "done"
Can't tick all? You skipped TDD. Go back.