nixos/shared/linked-dotfiles/opencode/skills/do-job/references/tdd-workflow.md
2025-10-27 11:56:37 -06:00

3.9 KiB

Test-Driven Development Workflow

Core TDD Cycle

  1. Red - Write failing test
  2. Green - Make test pass
  3. Refactor - Improve code while keeping tests passing

Writing Effective Tests

Test Structure

# Arrange - Set up test data and conditions
# Act - Execute the code being tested
# Assert - Verify expected behavior

Good Test Characteristics

  • Isolated - Tests don't depend on each other
  • Repeatable - Same input always produces same output
  • Fast - Tests run quickly
  • Clear - Test name describes what's being tested
  • Focused - One concept per test

Test Naming

test_<function>_<scenario>_<expected_result>

Examples:

  • test_rename_folder_as_owner_succeeds
  • test_rename_folder_without_permission_returns_403
  • test_rename_folder_with_empty_name_returns_400

Common Patterns

Testing Error Conditions

// Test expected errors
test('rename_folder_without_permission_returns_403', async () => {
  // Arrange: Set up user without permissions
  const user = createUserWithoutPermissions();
  
  // Act: Attempt rename
  const response = await renameFolder(user, folderId, newName);
  
  // Assert: Verify 403 error
  expect(response.status).toBe(403);
  expect(response.error).toContain('forbidden');
});

Testing Happy Path

test('rename_folder_as_owner_succeeds', async () => {
  // Arrange: Set up folder with owner
  const owner = createOwner();
  const folder = createFolder(owner);
  
  // Act: Rename folder
  const response = await renameFolder(owner, folder.id, 'NewName');
  
  // Assert: Verify success
  expect(response.status).toBe(200);
  expect(response.data.name).toBe('NewName');
});

Testing Edge Cases

test('rename_folder_with_special_characters_sanitizes_name', async () => {
  const owner = createOwner();
  const folder = createFolder(owner);
  
  const response = await renameFolder(owner, folder.id, '<script>alert(1)</script>');
  
  expect(response.status).toBe(200);
  expect(response.data.name).not.toContain('<script>');
});

TDD Best Practices

Start Simple

Write simplest test first, then add complexity:

  1. Happy path with minimal data
  2. Add edge cases
  3. Add error conditions
  4. Add integration scenarios

One Test at a Time

  • Write one test
  • Watch it fail
  • Make it pass
  • Refactor if needed
  • Repeat

Keep Tests Independent

// ❌ Bad - Tests depend on order
test('create_folder', () => { /* creates folder with id=1 */ });
test('rename_folder', () => { /* assumes folder id=1 exists */ });

// ✅ Good - Each test sets up own data
test('create_folder', () => {
  const folder = createFolder();
  expect(folder.id).toBeDefined();
});

test('rename_folder', () => {
  const folder = createFolder(); // Own setup
  const result = renameFolder(folder.id, 'NewName');
  expect(result.name).toBe('NewName');
});

Test Behavior, Not Implementation

// ❌ Bad - Tests internal implementation
test('uses_postgres_query', () => {
  expect(mockDb.query).toHaveBeenCalledWith('UPDATE folders...');
});

// ✅ Good - Tests behavior
test('rename_updates_folder_name', () => {
  renameFolder(folderId, 'NewName');
  const folder = getFolder(folderId);
  expect(folder.name).toBe('NewName');
});

When to Refactor

Refactor when tests are green and you notice:

  • Duplication
  • Long functions
  • Unclear variable names
  • Complex conditionals
  • Hard-coded values

Always keep tests passing during refactoring

Handling Legacy Code

When adding tests to existing code without tests:

  1. Write characterization tests (document current behavior)
  2. Refactor to make code testable
  3. Add tests for new functionality
  4. Gradually improve test coverage

Test Coverage

Aim for coverage of:

  • All new code paths
  • Bug fixes (test the bug scenario)
  • Edge cases
  • Error conditions

Not aiming for 100% - focus on meaningful coverage.