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?

BenefitDescription
Design clarity Writing the test forces you to think about the interface before implementation
Scope controlYou only write code needed to pass the test
Regression preventionEvery fix is permanently guarded by its test
Documentation Tests document expected behavior with executable examples
ConfidenceRefactoring 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 TypeTarget
Core libraries80%+
Business logic90%+
Utilities70%+
UI/Presentation50%+
Generated codeExcluded

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