3.9 KiB
3.9 KiB
Test-Driven Development Workflow
Core TDD Cycle
- Red - Write failing test
- Green - Make test pass
- 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_succeedstest_rename_folder_without_permission_returns_403test_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:
- Happy path with minimal data
- Add edge cases
- Add error conditions
- 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:
- Write characterization tests (document current behavior)
- Refactor to make code testable
- Add tests for new functionality
- 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.