Test-Driven Development
Guidelines for test-first development across all projects in the Tom Framework workspace.
---
Core Principle
Every change begins with a test.
- Bugs become failing tests that reproduce the issue
- Features become failing tests that describe expected behavior
- The code is complete when all tests pass
---
The TDD Cycle
┌─────────────────────────────────────────────────────────────────────────────┐
│ TDD CYCLE │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────┐
│ RED │ Write a failing test
│ │ that describes expected behavior
└──────┬──────┘
│
▼
┌─────────────┐
│ GREEN │ Write minimum code
│ │ to make the test pass
└──────┬──────┘
│
▼
┌─────────────┐
│ REFACTOR │ Improve code quality
│ │ while keeping tests green
└──────┬──────┘
│
└────────────► Repeat
Red Phase
1. Write a test that fails 2. The test describes exactly what you want the code to do 3. Run the test and confirm it fails for the right reason
Green Phase
1. Write the simplest code that makes the test pass 2. Don't optimize or clean up yet 3. The goal is a passing test, not perfect code
Refactor Phase
1. Improve code structure while tests stay green 2. Apply clean code principles 3. Run tests after each change
---
Why Test First?
| Benefit | Description |
|---|---|
| Design clarity | Writing the test forces you to think about the interface before implementation |
| Scope control | You only write code needed to pass the test |
| Regression prevention | Every fix is permanently guarded by its test |
| Documentation | Tests document expected behavior with executable examples |
| Confidence | Refactoring is safe when tests exist |
---
When to Write Tests
Always Test First
- Bug fixes (test must reproduce the bug)
- New features (test must describe expected behavior)
- API changes (test must verify new contract)
- Edge cases (test must verify boundaries)
Test After (Exception Cases)
- Exploratory prototyping (but add tests before merging)
- Spike investigations (discard code, write tests for real implementation)
- Emergency hotfixes (add test immediately after)
---
Test Granularity
Unit Tests
- Test individual functions and methods
- Fast, isolated, deterministic
- Mock external dependencies
Integration Tests
- Test component interactions
- May use real dependencies
- Slower but higher confidence
Choosing the Right Level
Is the behavior I'm testing...
├── A single function/method? → Unit test
├── Multiple components working together? → Integration test
├── User-visible workflow? → Integration or E2E test
└── External service interaction? → Integration test with mocks
---
Test Coverage Expectations
Minimum Coverage
| Component Type | Target |
|---|---|
| Core libraries | 80%+ |
| Business logic | 90%+ |
| Utilities | 70%+ |
| UI/Presentation | 50%+ |
| Generated code | Excluded |
Quality Over Quantity
- Coverage percentage is a guide, not a goal
- One good test > many superficial tests
- Test behavior, not implementation details
---
Writing Good Tests
Test Names
Describe the scenario and expected outcome:
// Good
test('should return empty list when no items match filter', () { ... });
test('should throw ArgumentError when input is negative', () { ... });
// Bad
test('test1', () { ... });
test('items', () { ... });
Arrange-Act-Assert
test('should calculate total with discount', () {
// Arrange
final cart = ShoppingCart();
cart.addItem(Item(price: 100));
cart.applyDiscount(0.1);
// Act
final total = cart.calculateTotal();
// Assert
expect(total, equals(90));
});
One Assertion Per Test (Guideline)
Prefer focused tests, but multiple assertions are acceptable when testing a single concept:
// Acceptable: related assertions about one result
test('should parse date correctly', () {
final date = parser.parse('2026-02-14');
expect(date.year, equals(2026));
expect(date.month, equals(2));
expect(date.day, equals(14));
});
---
Test-Driven Bug Fixing
1. Reproduce - Write a test that fails due to the bug 2. Verify - Confirm the test fails for the right reason 3. Fix - Make the minimum change to pass the test 4. Expand - Add edge case tests if the fix revealed other issues 5. Refactor - Clean up while tests stay green
---
Test-Driven Feature Development
1. Understand - Clarify requirements, identify test cases 2. Stub - Create class/method stubs that compile but throw/return null 3. Test first - Write failing tests for the first slice 4. Implement - Write code until tests pass 5. Repeat - Add more tests, implement more slices 6. Refactor - Clean up when all tests pass
---
Common Pitfalls
Testing Implementation
// Bad: Tests internal state
test('should set _internalFlag to true', () {
widget.doSomething();
expect(widget._internalFlag, isTrue); // Don't test private state
});
// Good: Tests observable behavior
test('should emit event when activated', () {
widget.doSomething();
expect(widget.isActive, isTrue);
});
Brittle Tests
// Bad: Test depends on exact output format
expect(result, equals('Error: Invalid input "foo" at position 3'));
// Good: Test checks the essential information
expect(result, contains('Invalid input'));
expect(result, contains('foo'));
Missing Edge Cases
Always test: - Empty inputs - Boundary values - Null/missing data (where applicable) - Error conditions - Concurrent access (where applicable)
---
See Also
- Unit Tests - Dart-specific test patterns and matchers
- Issue Implementation - Full workflow for bugs and features
- Clean Code Principles - Refactoring guidance