Testing and Code Quality with Claude Code
Tests are the foundation of a maintainable project. They're not just a safety net against bugs, but also executable documentation that describes how the code should behave. Claude Code can drastically accelerate test writing, from TDD to guided refactoring, from assisted debugging to edge case coverage.
In this article, we'll explore how to leverage Claude Code for every level of the testing pyramid, with specific prompts, practical examples, and best practices for obtaining robust and maintainable tests.
Series Overview
| # | Article | Focus |
|---|---|---|
| 1 | Foundation and Mindset | Setup and mindset |
| 2 | Ideation and Requirements | From idea to MVP |
| 3 | Backend Architecture | API and database |
| 4 | Frontend Structure | UI and components |
| 5 | Context and Setup | CLAUDE.md and project context |
| 6 | Prompt Engineering | Advanced prompts and agents |
| 7 | You are here - Testing and Quality | Unit, integration, E2E |
| 8 | Documentation | README, API docs, ADR |
| 9 | Deploy and DevOps | Docker, CI/CD |
| 10 | Evolution | Scalability and maintenance |
The Testing Pyramid
The testing pyramid defines the optimal strategy to balance speed, coverage, and maintainability. Claude Code can help you at all levels.
Testing Pyramid
/\
/ \
/ E2E\ - Few (5-10%)
/------\ Slow, expensive, fragile
/ \ Test complete user flows
/Integration\ - Moderate (15-25%)
/------------\ Medium speed
/ \ Test interactions between components
/ Unit Tests \ - Many (70-80%)
/------------------\ Fast, isolated, stable
/ \ Test individual units
/______________________\
Characteristics by Level
| Level | Speed | Isolation | Cost | Confidence |
|---|---|---|---|---|
| Unit | ~1ms | High | Low | Internal logic |
| Integration | ~100ms | Medium | Medium | Interactions |
| E2E | ~5s | Low | High | Complete system |
Testing Strategy with Claude Code
Claude Code excels at generating comprehensive tests because it can understand your entire codebase context. Here's how to leverage this capability.
Help me define a testing strategy for my project:
PROJECT CONTEXT:
- Type: E-commerce API
- Stack: Node.js + TypeScript + Express + PostgreSQL
- Critical paths: Authentication, Checkout, Payment
- Current coverage: 45%
- Target coverage: 80%
- Team size: 2 developers
- Time available: 2 weeks
EXISTING TESTS:
- Some unit tests for utilities
- No integration tests
- No E2E tests
PROVIDE:
1. Priority order for what to test first
2. Recommended test types for each component
3. Coverage targets by component
4. Time estimates for each area
5. Testing tools recommendations
6. CI/CD integration plan
Unit Tests: The Base of the Pyramid
Unit tests verify individual functions or classes in complete isolation. They're fast, reliable, and provide immediate feedback during development.
Prompts for Generating Unit Tests
Generate comprehensive unit tests for this service:
```typescript
[PASTE SERVICE CODE HERE]
```
TESTING REQUIREMENTS:
- Framework: Jest with TypeScript
- Coverage target: >90% for this critical service
- Mock ALL external dependencies (repositories, external services)
TEST CATEGORIES TO COVER:
1. Happy Path: Normal successful operations
2. Validation: Invalid inputs, edge cases
3. Error Handling: Expected errors are thrown correctly
4. Edge Cases: Empty arrays, null values, boundary conditions
5. State Changes: Verify side effects (calls to dependencies)
OUTPUT FORMAT:
- Complete test file with all imports
- Use describe/it blocks with clear descriptions
- Include beforeEach for setup
- Group tests by method
- Use AAA pattern (Arrange, Act, Assert)
- Add comments explaining non-obvious test cases
NAMING CONVENTION:
- Describe: "ServiceName"
- Nested describe: "methodName"
- It: "should [expected behavior] when [condition]"
Complete Example: UserService Unit Tests
import {{ '{' }} UserService {{ '}' }} from './user.service';
import {{ '{' }} UserRepository {{ '}' }} from './user.repository';
import {{ '{' }} EmailService {{ '}' }} from '@shared/services/email.service';
import {{ '{' }} ValidationError, NotFoundError, ConflictError {{ '}' }} from '@shared/errors';
import {{ '{' }} createUserFixture, validUserDto {{ '}' }} from './fixtures/user.fixtures';
describe('UserService', () => {{ '{' }}
let service: UserService;
let mockUserRepository: jest.Mocked<UserRepository>;
let mockEmailService: jest.Mocked<EmailService>;
beforeEach(() => {{ '{' }}
// Create fresh mocks for each test
mockUserRepository = {{ '{' }}
findById: jest.fn(),
findByEmail: jest.fn(),
create: jest.fn(),
update: jest.fn(),
softDelete: jest.fn(),
findAll: jest.fn(),
{{ '}' }} as any;
mockEmailService = {{ '{' }}
sendWelcomeEmail: jest.fn(),
sendPasswordResetEmail: jest.fn(),
{{ '}' }} as any;
service = new UserService(mockUserRepository, mockEmailService);
{{ '}' }});
afterEach(() => {{ '{' }}
jest.clearAllMocks();
{{ '}' }});
// ===============================================================
// CREATE USER
// ===============================================================
describe('createUser', () => {{ '{' }}
describe('happy path', () => {{ '{' }}
it('should create user and send welcome email when valid data provided', async () => {{ '{' }}
// Arrange
const dto = validUserDto;
const expectedUser = createUserFixture({{ '{' }} id: 'user-123', ...dto {{ '}' }});
mockUserRepository.findByEmail.mockResolvedValue(null);
mockUserRepository.create.mockResolvedValue(expectedUser);
mockEmailService.sendWelcomeEmail.mockResolvedValue(undefined);
// Act
const result = await service.createUser(dto);
// Assert
expect(result.id).toBe('user-123');
expect(result.email).toBe(dto.email);
expect(mockUserRepository.create).toHaveBeenCalledWith(
expect.objectContaining({{ '{' }}
name: dto.name,
email: dto.email,
// Password should be hashed, not plain text
password: expect.not.stringContaining(dto.password),
{{ '}' }})
);
expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith(
expectedUser.email,
expectedUser.name
);
{{ '}' }});
it('should hash password before saving', async () => {{ '{' }}
// Arrange
const dto = {{ '{' }} ...validUserDto, password: 'PlainPassword123!' {{ '}' }};
mockUserRepository.findByEmail.mockResolvedValue(null);
mockUserRepository.create.mockResolvedValue(createUserFixture());
// Act
await service.createUser(dto);
// Assert
const createCall = mockUserRepository.create.mock.calls[0][0];
expect(createCall.password).not.toBe('PlainPassword123!');
expect(createCall.password.length).toBeGreaterThan(50); // bcrypt hash length
{{ '}' }});
{{ '}' }});
describe('validation errors', () => {{ '{' }}
it('should throw ConflictError when email already exists', async () => {{ '{' }}
// Arrange
const existingUser = createUserFixture({{ '{' }} email: 'exists@test.com' {{ '}' }});
mockUserRepository.findByEmail.mockResolvedValue(existingUser);
// Act & Assert
await expect(
service.createUser({{ '{' }} ...validUserDto, email: 'exists@test.com' {{ '}' }})
).rejects.toThrow(ConflictError);
expect(mockUserRepository.create).not.toHaveBeenCalled();
expect(mockEmailService.sendWelcomeEmail).not.toHaveBeenCalled();
{{ '}' }});
it('should throw ValidationError when password is too weak', async () => {{ '{' }}
// Arrange
mockUserRepository.findByEmail.mockResolvedValue(null);
// Act & Assert
await expect(
service.createUser({{ '{' }} ...validUserDto, password: '123' {{ '}' }})
).rejects.toThrow(ValidationError);
{{ '}' }});
{{ '}' }});
describe('edge cases', () => {{ '{' }}
it('should trim whitespace from email', async () => {{ '{' }}
// Arrange
mockUserRepository.findByEmail.mockResolvedValue(null);
mockUserRepository.create.mockResolvedValue(createUserFixture());
// Act
await service.createUser({{ '{' }}
...validUserDto,
email: ' user@test.com '
{{ '}' }});
// Assert
expect(mockUserRepository.findByEmail).toHaveBeenCalledWith('user@test.com');
{{ '}' }});
it('should normalize email to lowercase', async () => {{ '{' }}
// Arrange
mockUserRepository.findByEmail.mockResolvedValue(null);
mockUserRepository.create.mockResolvedValue(createUserFixture());
// Act
await service.createUser({{ '{' }}
...validUserDto,
email: 'User@TEST.com'
{{ '}' }});
// Assert
expect(mockUserRepository.findByEmail).toHaveBeenCalledWith('user@test.com');
{{ '}' }});
{{ '}' }});
{{ '}' }});
// ===============================================================
// GET USER BY ID
// ===============================================================
describe('getUserById', () => {{ '{' }}
it('should return user when found', async () => {{ '{' }}
// Arrange
const user = createUserFixture({{ '{' }} id: 'user-123' {{ '}' }});
mockUserRepository.findById.mockResolvedValue(user);
// Act
const result = await service.getUserById('user-123');
// Assert
expect(result).toEqual(user);
expect(mockUserRepository.findById).toHaveBeenCalledWith('user-123');
{{ '}' }});
it('should throw NotFoundError when user does not exist', async () => {{ '{' }}
// Arrange
mockUserRepository.findById.mockResolvedValue(null);
// Act & Assert
await expect(service.getUserById('non-existent'))
.rejects.toThrow(NotFoundError);
{{ '}' }});
{{ '}' }});
// ===============================================================
// SOFT DELETE USER
// ===============================================================
describe('softDeleteUser', () => {{ '{' }}
it('should soft delete existing user', async () => {{ '{' }}
// Arrange
const user = createUserFixture({{ '{' }} id: 'user-123' {{ '}' }});
mockUserRepository.findById.mockResolvedValue(user);
mockUserRepository.softDelete.mockResolvedValue(undefined);
// Act
await service.softDeleteUser('user-123');
// Assert
expect(mockUserRepository.softDelete).toHaveBeenCalledWith('user-123');
{{ '}' }});
it('should throw NotFoundError when user does not exist', async () => {{ '{' }}
// Arrange
mockUserRepository.findById.mockResolvedValue(null);
// Act & Assert
await expect(service.softDeleteUser('non-existent'))
.rejects.toThrow(NotFoundError);
expect(mockUserRepository.softDelete).not.toHaveBeenCalled();
{{ '}' }});
{{ '}' }});
{{ '}' }});
Integration Tests: Testing Interactions
Integration tests verify that components work correctly together. They test the complete API flow, including routing, middleware, validation, and database access.
Prompt for Integration Tests
Generate integration tests for this API:
ENDPOINTS:
- POST /api/users - Create user (requires auth)
- GET /api/users/:id - Get user by ID
- PATCH /api/users/:id - Update user (owner only)
- DELETE /api/users/:id - Soft delete (admin only)
TESTING REQUIREMENTS:
- Framework: Jest + Supertest
- Database: Test PostgreSQL (use transactions, rollback after each test)
- Auth: JWT tokens (mock or real test tokens)
TEST SCENARIOS FOR EACH ENDPOINT:
1. Success case with valid request
2. Validation errors (400) - invalid body, missing fields
3. Authentication errors (401) - missing/invalid token
4. Authorization errors (403) - wrong role/permissions
5. Not found errors (404) - resource doesn't exist
6. Conflict errors (409) - duplicate resources
SETUP/TEARDOWN:
- beforeAll: Create test database, run migrations
- beforeEach: Start transaction
- afterEach: Rollback transaction
- afterAll: Close database connection
Include helper functions for:
- Creating authenticated requests
- Generating test users
- Cleaning up test data
Complete Integration Test Example
import request from 'supertest';
import {{ '{' }} app {{ '}' }} from '../app';
import {{ '{' }} db {{ '}' }} from '../database';
import {{ '{' }} createTestUser, getAuthToken, cleanupTestData {{ '}' }} from './helpers';
import {{ '{' }} UserRole {{ '}' }} from '@shared/types';
describe('Users API Integration Tests', () => {{ '{' }}
let adminToken: string;
let userToken: string;
let testUserId: string;
beforeAll(async () => {{ '{' }}
await db.connect();
await db.migrate();
// Create test users and get tokens
const admin = await createTestUser({{ '{' }} role: UserRole.ADMIN {{ '}' }});
const user = await createTestUser({{ '{' }} role: UserRole.USER {{ '}' }});
adminToken = await getAuthToken(admin);
userToken = await getAuthToken(user);
testUserId = user.id;
{{ '}' }});
afterAll(async () => {{ '{' }}
await cleanupTestData();
await db.disconnect();
{{ '}' }});
// ===============================================================
// POST /api/users - Create User
// ===============================================================
describe('POST /api/users', () => {{ '{' }}
const validPayload = {{ '{' }}
name: 'Integration Test User',
email: 'integration@test.com',
password: 'SecurePass123!',
{{ '}' }};
describe('success cases', () => {{ '{' }}
it('should create user with valid data (201)', async () => {{ '{' }}
const response = await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${{ '{' }}adminToken{{ '}' }}`)
.send({{ '{' }} ...validPayload, email: `unique-${{ '{' }}Date.now(){{ '}' }}@test.com` {{ '}' }});
expect(response.status).toBe(201);
expect(response.body.data).toMatchObject({{ '{' }}
name: validPayload.name,
email: expect.stringContaining('@test.com'),
{{ '}' }});
expect(response.body.data.password).toBeUndefined(); // Never return password
expect(response.body.data.id).toBeDefined();
{{ '}' }});
{{ '}' }});
describe('validation errors (400)', () => {{ '{' }}
it('should return 400 when email is invalid', async () => {{ '{' }}
const response = await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${{ '{' }}adminToken{{ '}' }}`)
.send({{ '{' }} ...validPayload, email: 'not-an-email' {{ '}' }});
expect(response.status).toBe(400);
expect(response.body.error.code).toBe('VALIDATION_ERROR');
{{ '}' }});
it('should return 400 when password is too weak', async () => {{ '{' }}
const response = await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${{ '{' }}adminToken{{ '}' }}`)
.send({{ '{' }} ...validPayload, password: '123' {{ '}' }});
expect(response.status).toBe(400);
{{ '}' }});
{{ '}' }});
describe('authentication errors (401)', () => {{ '{' }}
it('should return 401 without auth token', async () => {{ '{' }}
const response = await request(app)
.post('/api/users')
.send(validPayload);
expect(response.status).toBe(401);
expect(response.body.error.code).toBe('UNAUTHORIZED');
{{ '}' }});
it('should return 401 with invalid token', async () => {{ '{' }}
const response = await request(app)
.post('/api/users')
.set('Authorization', 'Bearer invalid-token')
.send(validPayload);
expect(response.status).toBe(401);
{{ '}' }});
{{ '}' }});
describe('conflict errors (409)', () => {{ '{' }}
it('should return 409 when email already exists', async () => {{ '{' }}
// First, create a user
const email = `conflict-${{ '{' }}Date.now(){{ '}' }}@test.com`;
await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${{ '{' }}adminToken{{ '}' }}`)
.send({{ '{' }} ...validPayload, email {{ '}' }});
// Try to create with same email
const response = await request(app)
.post('/api/users')
.set('Authorization', `Bearer ${{ '{' }}adminToken{{ '}' }}`)
.send({{ '{' }} ...validPayload, email {{ '}' }});
expect(response.status).toBe(409);
expect(response.body.error.code).toBe('CONFLICT');
{{ '}' }});
{{ '}' }});
{{ '}' }});
// ===============================================================
// DELETE /api/users/:id - Soft Delete (Admin Only)
// ===============================================================
describe('DELETE /api/users/:id', () => {{ '{' }}
it('should return 403 when non-admin tries to delete', async () => {{ '{' }}
const response = await request(app)
.delete(`/api/users/${{ '{' }}testUserId{{ '}' }}`)
.set('Authorization', `Bearer ${{ '{' }}userToken{{ '}' }}`);
expect(response.status).toBe(403);
expect(response.body.error.code).toBe('FORBIDDEN');
{{ '}' }});
it('should soft delete user when admin (204)', async () => {{ '{' }}
// Create user to delete
const userToDelete = await createTestUser();
const response = await request(app)
.delete(`/api/users/${{ '{' }}userToDelete.id{{ '}' }}`)
.set('Authorization', `Bearer ${{ '{' }}adminToken{{ '}' }}`);
expect(response.status).toBe(204);
{{ '}' }});
{{ '}' }});
{{ '}' }});
E2E Tests: Complete User Flows
End-to-end tests simulate real user behavior, testing the entire application from browser to database.
Prompt for E2E Tests
Generate E2E tests for user registration and login flow:
FLOW TO TEST:
1. User visits homepage
2. Clicks "Sign Up" button
3. Fills registration form (name, email, password)
4. Submits form
5. Sees success message
6. Is redirected to dashboard
7. Can log out and log back in
FRAMEWORK: Playwright with TypeScript
TEST SCENARIOS:
1. Happy path: Complete registration and login
2. Validation: Form shows errors for invalid input
3. Duplicate email: Shows appropriate error
4. Login with wrong password: Shows error
5. Remember me: Session persists after browser close
INCLUDE:
- Page Object Model for reusable selectors
- Helper functions for common actions
- Screenshot on failure
- Retry logic for flaky network requests
E2E Example with Playwright
import {{ '{' }} test, expect {{ '}' }} from '@playwright/test';
import {{ '{' }} LoginPage {{ '}' }} from './pages/login.page';
import {{ '{' }} RegisterPage {{ '}' }} from './pages/register.page';
import {{ '{' }} DashboardPage {{ '}' }} from './pages/dashboard.page';
import {{ '{' }} generateTestEmail {{ '}' }} from './helpers';
test.describe('User Authentication Flow', () => {{ '{' }}
test.describe('Registration', () => {{ '{' }}
test('should complete registration successfully', async ({{ '{' }} page {{ '}' }}) => {{ '{' }}
const registerPage = new RegisterPage(page);
const dashboardPage = new DashboardPage(page);
const testEmail = generateTestEmail();
// Navigate to registration
await registerPage.goto();
await expect(page).toHaveTitle(/Sign Up/);
// Fill form
await registerPage.fillName('Test User');
await registerPage.fillEmail(testEmail);
await registerPage.fillPassword('SecurePass123!');
await registerPage.fillConfirmPassword('SecurePass123!');
// Submit and verify
await registerPage.submit();
// Should redirect to dashboard
await expect(page).toHaveURL(/\/dashboard/);
await expect(dashboardPage.welcomeMessage).toContainText('Welcome, Test User');
{{ '}' }});
test('should show validation errors for invalid input', async ({{ '{' }} page {{ '}' }}) => {{ '{' }}
const registerPage = new RegisterPage(page);
await registerPage.goto();
// Submit empty form
await registerPage.submit();
// Check validation messages
await expect(registerPage.nameError).toBeVisible();
await expect(registerPage.emailError).toBeVisible();
await expect(registerPage.passwordError).toBeVisible();
{{ '}' }});
{{ '}' }});
test.describe('Login', () => {{ '{' }}
test('should login with valid credentials', async ({{ '{' }} page {{ '}' }}) => {{ '{' }}
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
await loginPage.goto();
await loginPage.fillEmail('test@example.com');
await loginPage.fillPassword('TestPass123!');
await loginPage.submit();
await expect(page).toHaveURL(/\/dashboard/);
await expect(dashboardPage.userMenu).toBeVisible();
{{ '}' }});
test('should show error for invalid credentials', async ({{ '{' }} page {{ '}' }}) => {{ '{' }}
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.fillEmail('test@example.com');
await loginPage.fillPassword('WrongPassword');
await loginPage.submit();
await expect(loginPage.errorMessage).toContainText('Invalid credentials');
await expect(page).toHaveURL(/\/login/);
{{ '}' }});
{{ '}' }});
{{ '}' }});
TDD Workflow with Claude Code
Test-Driven Development (TDD) works exceptionally well with Claude Code. Write the test first, then let Claude suggest the implementation.
TDD Workflow
| Step | You | Claude Code |
|---|---|---|
| 1. Red | Write failing test | Suggests test cases |
| 2. Green | Ask for implementation | Generates minimal code |
| 3. Refactor | Ask for improvements | Proposes refactoring |
| 4. Repeat | Add new test | Suggests edge cases |
I'm following TDD. Here's my failing test:
```typescript
describe('calculateDiscount', () => {{ '{' }}
it('should apply 10% discount for orders over $100', () => {{ '{' }}
expect(calculateDiscount(150)).toBe(15);
{{ '}' }});
it('should apply 20% discount for orders over $500', () => {{ '{' }}
expect(calculateDiscount(600)).toBe(120);
{{ '}' }});
it('should return 0 for orders under $100', () => {{ '{' }}
expect(calculateDiscount(50)).toBe(0);
{{ '}' }});
{{ '}' }});
```
1. Implement the MINIMUM code to make these tests pass
2. Don't add features not covered by tests
3. After implementation, suggest additional edge cases I should test
Guided Refactoring with Tests
Existing tests allow you to refactor safely. Claude Code can suggest improvements while maintaining behavior.
I want to refactor this code. I have tests that cover its behavior:
CURRENT CODE:
```typescript
[PASTE CODE]
```
EXISTING TESTS (all passing):
```typescript
[PASTE TESTS]
```
REFACTORING GOALS:
1. Reduce cyclomatic complexity
2. Extract reusable functions
3. Improve naming
CONSTRAINTS:
- All existing tests MUST still pass
- Don't change public interface
- No new dependencies
Provide:
1. Refactored code
2. Explanation of each change
3. Any new tests needed for extracted functions
4. Run the tests and confirm they pass
Assisted Debugging with Claude Code
Claude Code excels at debugging because it can see your entire codebase and understand the context of failures.
This test is failing and I can't figure out why:
TEST:
```typescript
it('should apply discount to order total', async () => {{ '{' }}
const order = await orderService.createOrder({{ '{' }}
items: [{{ '{' }} productId: 'prod-1', quantity: 2 {{ '}' }}],
couponCode: 'SAVE10',
{{ '}' }});
expect(order.total).toBe(90); // Expected $90 after 10% discount
expect(order.discount).toBe(10);
{{ '}' }});
```
ERROR:
```
Expected: 90
Received: 100
```
IMPLEMENTATION:
```typescript
async createOrder(dto: CreateOrderDto): Promise<Order> {{ '{' }}
const items = await this.getOrderItems(dto.items);
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
let discount = 0;
if (dto.couponCode) {{ '{' }}
const coupon = await this.couponRepository.findByCode(dto.couponCode);
if (coupon) {{ '{' }}
discount = subtotal * (coupon.percentage / 100);
{{ '}' }}
{{ '}' }}
return this.orderRepository.create({{ '{' }}
items,
subtotal,
discount,
total: subtotal - discount,
{{ '}' }});
{{ '}' }}
```
Help me:
1. Identify why the test is failing
2. Determine if it's a test bug or implementation bug
3. Provide the fix
4. Suggest how to prevent similar issues
Code Review with Claude Code
Use Claude Code for thorough code reviews that catch issues before they reach production.
Review these tests for quality and completeness:
```typescript
[PASTE TEST FILE]
```
EVALUATE:
1. Test Coverage
- Are all code paths tested?
- Are edge cases covered?
- Are error scenarios tested?
2. Test Quality
- Are tests isolated (no shared state)?
- Do tests follow AAA pattern?
- Are assertions meaningful?
- Are tests readable and maintainable?
3. Anti-Patterns
- Tests that test implementation, not behavior
- Excessive mocking (testing mocks, not code)
- Tests that depend on execution order
- Flaky tests (race conditions, timing issues)
4. Missing Tests
- What scenarios are not covered?
- What edge cases are missing?
- What error paths need testing?
PROVIDE:
- Overall assessment (Poor / Adequate / Good / Excellent)
- Specific issues found
- Missing test cases to add
- Refactoring suggestions
Test Coverage and Gap Analysis
Use Claude Code to identify uncovered code and generate missing tests.
My coverage report shows uncovered lines:
FILE: src/services/order.service.ts
UNCOVERED LINES: 45-52, 78-85, 120-135
Here's the code at those lines:
```typescript
// Lines 45-52
if (order.status === 'cancelled') {{ '{' }}
throw new ValidationError('Cannot modify cancelled order');
{{ '}' }}
// Lines 78-85
if (items.some(item => item.quantity > item.stock)) {{ '{' }}
const outOfStock = items.filter(i => i.quantity > i.stock);
throw new ValidationError(`Insufficient stock for: ${{ '{' }}outOfStock.map(i => i.name).join(', '){{ '}' }}`);
{{ '}' }}
// Lines 120-135
if (coupon && coupon.expiresAt < new Date()) {{ '{' }}
throw new ValidationError('Coupon has expired');
{{ '}' }}
if (coupon && coupon.usageCount >= coupon.maxUsage) {{ '{' }}
throw new ValidationError('Coupon usage limit reached');
{{ '}' }}
```
Generate unit tests to cover these scenarios. Include:
1. Test for each branch
2. Boundary conditions
3. Descriptive test names
4. Proper mocking setup
Test Fixtures and Factories
Well-structured fixtures make tests more readable and maintainable. Ask Claude Code to create factory functions.
Create test fixtures and factory functions for my User entity:
ENTITY:
```typescript
interface User {{ '{' }}
id: string;
name: string;
email: string;
password: string;
role: 'admin' | 'user' | 'premium';
status: 'active' | 'inactive' | 'deleted';
createdAt: Date;
updatedAt: Date;
deletedAt: Date | null;
premiumExpiresAt?: Date;
{{ '}' }}
```
CREATE:
1. validUserDto - Default valid input for creating user
2. createUserFixture(overrides) - Factory function with defaults
3. invalidUserDtos - Object with various invalid inputs
4. userScenarios - Common test scenarios (admin, inactive, deleted, etc.)
5. resetFixtures() - Function to reset any counters
Follow these patterns:
- Use incrementing IDs for uniqueness
- Include common edge cases
- Make it easy to override any field
Testing Best Practices
Anti-Patterns
- Tests that depend on order
- Shared state between tests
- Vague names: "should work"
- Multiple unrelated assertions
- Testing implementation details
- Excessive mocking
- Slow tests without reason
- Ignoring flaky tests
Best Practices
- Independent and isolated tests
- Fresh setup for each test
- Descriptive names with context
- One logical assertion per test
- Test behavior, not implementation
- Only mock external dependencies
- Optimize shared setup
- Fix or remove flaky tests
Test Quality Checklist
Before Merging
- All tests pass locally
- Coverage >= 80% for critical code
- No flaky tests (run 3 times)
- Test names are descriptive
- Edge cases are covered
- Error paths are tested
- No console.log in tests
- Fixtures are clean and reusable
- Mocks are realistic
- Tests are fast (unit < 10s total)
Conclusion and Next Steps
Testing is where Claude Code truly shines. It can generate complete tests from existing code, suggest edge cases you might not have considered, and help you maintain high coverage without sacrificing development speed.
Remember: tests generated by Claude Code must always be verified. AI can generate tests that pass but don't actually test the important behavior, or that are too tied to implementation rather than behavior.
In the next article we'll see how to use Claude Code for professional documentation: complete READMEs, API documentation with OpenAPI, Architecture Decision Records, and JSDoc.
Key Points to Remember
- Pyramid: 70% unit, 20% integration, 10% E2E
- Unit: Complete isolation, mock dependencies
- Integration: Test the API end-to-end with real DB
- E2E: Simulate real user behavior
- TDD: Write tests first, implement after
- Coverage: Aim for 80%+ on critical code
- Fixtures: Factory functions for reusable data
- Review: Use Claude for test quality checks
- Verification: Always check generated tests
Commenti
Caricamento commenti...
Sign in to comment