| name | testing |
| description | Testing patterns, frameworks, and best practices for unit, integration, and end-to-end tests. Use when writing tests, implementing TDD/BDD, or when working with test frameworks in any language. |
Testing Best Practices
This skill provides testing patterns, frameworks, and best practices applicable across multiple programming languages and frameworks.
When to Use
✅ DO Write Tests For:
- Business logic and domain rules
- API endpoints and routes
- Service methods and utilities
- Error handling and edge cases
- Integration points (external services, databases)
- Critical user flows
❌ DON'T Write Tests For:
- Simple getters/setters
- Framework code (unless custom)
- Third-party library code
- Trivial code with no logic
- Generated code
Core Concepts
Test Types
Unit Tests:
- Test individual functions/methods in isolation
- Fast execution (< 1ms per test)
- No external dependencies (mocked)
- High code coverage
Integration Tests:
- Test multiple components working together
- May include database, file system, or external services
- Slower execution
- Verify contracts and interfaces
End-to-End (E2E) Tests:
- Test complete user workflows
- Full system stack
- Slowest execution
- Verify system behavior from user perspective
Test Structure (AAA Pattern)
Arrange: Set up test data and conditions
Act: Execute the code under test
Assert: Verify the expected outcome
describe('OrderService', () => {
it('should calculate total with tax', () => {
const order = { items: [{ price: 100 }], taxRate: 0.1 };
const total = calculateTotal(order);
expect(total).toBe(110);
});
});
public class OrderServiceTests
{
[Fact]
public void CalculateTotal_WithTax_ReturnsCorrectTotal()
{
var order = new Order { Items = new[] { new Item { Price = 100 } }, TaxRate = 0.1m };
var total = _service.CalculateTotal(order);
Assert.Equal(110m, total);
}
}
Testing Patterns
Pattern 1: Unit Test Structure
Naming Convention:
MethodName_Scenario_ExpectedBehavior
- Example:
ProcessPayment_WithValidCard_ReturnsSuccess
Test Organization:
describe('ComponentName', () => {
describe('methodName', () => {
it('should do X when Y', () => {
});
it('should handle Z error', () => {
});
});
});
Pattern 2: Mocking External Dependencies
Dependency Injection for Testability:
export class OrderService {
constructor(
private paymentClient: PaymentClient,
private emailService: EmailService
) {}
async processOrder(order: Order) {
const payment = await this.paymentClient.charge(order);
await this.emailService.sendConfirmation(order);
return payment;
}
}
describe('OrderService', () => {
it('should process order successfully', async () => {
const mockPaymentClient = {
charge: jest.fn().mockResolvedValue({ success: true })
};
const mockEmailService = {
sendConfirmation: jest.fn().mockResolvedValue(undefined)
};
const service = new OrderService(mockPaymentClient, mockEmailService);
const result = await service.processOrder(testOrder);
expect(result.success).toBe(true);
expect(mockPaymentClient.charge).toHaveBeenCalledWith(testOrder);
expect(mockEmailService.sendConfirmation).toHaveBeenCalledWith(testOrder);
});
});
Pattern 3: Testing Async Operations
it('should handle async operations', async () => {
const result = await asyncFunction();
expect(result).toBeDefined();
});
it('should reject on error', async () => {
await expect(asyncFunction()).rejects.toThrow('Error message');
});
Pattern 4: Testing Error Cases
it('should throw error for invalid input', () => {
expect(() => {
processPayment(null);
}).toThrow('Payment request is required');
});
it('should handle network errors gracefully', async () => {
const mockClient = {
charge: jest.fn().mockRejectedValue(new Error('Network error'))
};
await expect(service.processOrder(order)).rejects.toThrow('Network error');
});
Pattern 5: Test Data Builders
class OrderBuilder {
private order: Partial<Order> = {};
withId(id: string) {
this.order.id = id;
return this;
}
withItems(items: Item[]) {
this.order.items = items;
return this;
}
build(): Order {
return { ...defaultOrder, ...this.order } as Order;
}
}
const order = new OrderBuilder()
.withId('123')
.withItems([{ price: 100 }])
.build();
Test Frameworks
JavaScript/TypeScript
Jest:
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
describe('MyComponent', () => {
beforeEach(() => {
});
it('should work', () => {
expect(result).toBe(expected);
});
});
Vitest:
import { describe, it, expect } from 'vitest';
Mocha + Chai:
import { describe, it } from 'mocha';
import { expect } from 'chai';
describe('MyComponent', () => {
it('should work', () => {
expect(result).to.equal(expected);
});
});
C#/.NET
xUnit:
public class MyServiceTests
{
[Fact]
public void ShouldWork()
{
Assert.Equal(expected, actual);
}
[Theory]
[InlineData(1, 2, 3)]
[InlineData(2, 3, 5)]
public void ShouldAdd(int a, int b, int expected)
{
Assert.Equal(expected, a + b);
}
}
NUnit:
[TestFixture]
public class MyServiceTests
{
[Test]
public void ShouldWork()
{
Assert.AreEqual(expected, actual);
}
}
Best Practices
1. Test Independence
- Each test should be independent and runnable in isolation
- Tests should not depend on execution order
- Clean up after each test (use
beforeEach/afterEach)
2. Test Naming
- Use descriptive names that explain what is being tested
- Follow pattern:
MethodName_Scenario_ExpectedBehavior
- Avoid generic names like
test1, test2
3. Arrange-Act-Assert (AAA)
- Clearly separate setup, execution, and verification
- Keep arrange section minimal
- One assertion per test when possible (or related assertions)
4. Test Coverage
- Aim for high coverage of business logic (> 80%)
- Don't obsess over 100% coverage
- Focus on critical paths and edge cases
5. Fast Tests
- Unit tests should be fast (< 1ms)
- Use mocks for slow operations
- Run fast tests frequently, slow tests less often
6. Test Maintainability
- Keep tests simple and readable
- Extract common setup into helper functions
- Use test data builders for complex objects
- Don't duplicate production logic in tests
Common Patterns
Pattern: Testing HTTP Endpoints
import request from 'supertest';
import app from '../app';
describe('GET /api/orders/:id', () => {
it('should return order when found', async () => {
const response = await request(app)
.get('/api/orders/123')
.expect(200);
expect(response.body).toHaveProperty('id', '123');
});
it('should return 404 when not found', async () => {
await request(app)
.get('/api/orders/999')
.expect(404);
});
});
Pattern: Testing Database Operations
describe('OrderRepository', () => {
beforeEach(async () => {
await testDb.clean();
});
it('should save and retrieve order', async () => {
const order = await repository.save(testOrder);
const retrieved = await repository.findById(order.id);
expect(retrieved).toMatchObject(testOrder);
});
});
Key Principles
- Test Behavior, Not Implementation: Test what the code does, not how it does it
- Fast Feedback: Keep tests fast for quick iteration
- Clear Intent: Test names and structure should clearly communicate intent
- Isolation: Tests should not depend on each other
- Maintainability: Tests should be as maintainable as production code
- Coverage with Purpose: High coverage is good, but meaningful tests are better