Unit Testing
Unit tests verify that individual functions and modules work correctly in isolation.
What to Test
✅ DO Test
- Business logic functions
- Utility functions
- Data transformations
- Validation functions
- Error handling
❌ DON'T Test
- Third-party libraries (they have their own tests)
- Framework code (Express, React, etc.)
- Database connections (test in integration tests)
- External API calls (mock them)
Writing Unit Tests
Example: Testing a Utility Function
// src/utils/email-validator.js
export function validateEmail(email) {
if (!email || typeof email !== 'string') {
throw new Error('Email is required');
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new Error('Invalid email format');
}
return email.trim().toLowerCase();
}
// tests/utils/email-validator.test.js
import { validateEmail } from '../../src/utils/email-validator.js';
describe('validateEmail', () => {
test('validates correct email', () => {
expect(validateEmail('test@example.com')).toBe('test@example.com');
});
test('normalizes email to lowercase', () => {
expect(validateEmail('Test@Example.COM')).toBe('test@example.com');
});
test('trims whitespace', () => {
expect(validateEmail(' test@example.com ')).toBe('test@example.com');
});
test('throws error for invalid email', () => {
expect(() => validateEmail('not-an-email')).toThrow('Invalid email format');
});
test('throws error for missing email', () => {
expect(() => validateEmail(null)).toThrow('Email is required');
expect(() => validateEmail(undefined)).toThrow('Email is required');
});
});
Example: Testing with Mocks
// src/services/email-service.js
import { sendEmail } from './email-provider.js';
export async function sendWelcomeEmail(user) {
return await sendEmail({
to: user.email,
subject: 'Welcome!',
body: `Hello ${user.name}, welcome to Reformer!`
});
}
// tests/services/email-service.test.js
import { sendWelcomeEmail } from '../../src/services/email-service.js';
import { sendEmail } from '../../src/services/email-provider.js';
jest.mock('../../src/services/email-provider.js');
describe('sendWelcomeEmail', () => {
test('sends email with correct content', async () => {
const user = { email: 'test@example.com', name: 'Test User' };
await sendWelcomeEmail(user);
expect(sendEmail).toHaveBeenCalledWith({
to: 'test@example.com',
subject: 'Welcome!',
body: 'Hello Test User, welcome to Reformer!'
});
});
});
Test Structure
Arrange-Act-Assert Pattern
test('description', () => {
// Arrange: Set up test data
const input = 'test@example.com';
// Act: Execute the function
const result = validateEmail(input);
// Assert: Verify the result
expect(result).toBe('test@example.com');
});
Best Practices
- One assertion per test (when possible)
- Descriptive test names - Describe what is being tested
- Test edge cases - Null, undefined, empty strings, etc.
- Test error cases - What happens when things go wrong
- Keep tests fast - Unit tests should run in milliseconds
- Keep tests isolated - Don't depend on other tests
- Use fixtures - Reusable test data
Running Unit Tests
# Run all unit tests
npm run test:unit
# Run specific test file
npm run test:unit -- email-validator.test.js
# Run with coverage
npm run test:unit -- --coverage
# Watch mode
npm run test:unit -- --watch
Coverage Goals
- Minimum: 70% coverage
- Target: 80%+ coverage
- Critical paths: 100% coverage
Common Patterns
Testing Async Functions
test('async function', async () => {
const result = await asyncFunction();
expect(result).toBe(expected);
});
Testing Error Cases
test('throws error on invalid input', () => {
expect(() => {
functionUnderTest(null);
}).toThrow('Invalid input');
});
Testing with Mocks
import { functionToMock } from './module.js';
jest.mock('./module.js');
test('uses mock', () => {
functionToMock.mockReturnValue('mocked value');
// ... test code
});