Skip to main content

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

  1. One assertion per test (when possible)
  2. Descriptive test names - Describe what is being tested
  3. Test edge cases - Null, undefined, empty strings, etc.
  4. Test error cases - What happens when things go wrong
  5. Keep tests fast - Unit tests should run in milliseconds
  6. Keep tests isolated - Don't depend on other tests
  7. 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
});

Resources