Unit Testing: Jest, Mocha, Vitest
Unit testing is fundamental for code quality. This article covers popular testing frameworks and best practices.
Introduction
Unit testing provides:
- Code quality assurance
- Regression prevention
- Documentation
- Refactoring confidence
- Bug detection
Understanding unit testing helps you:
- Write testable code
- Catch bugs early
- Maintain code quality
- Document behavior
- Refactor safely
Jest Testing Framework
Jest Setup
# โ
Good: Install Jest
npm install --save-dev jest
# โ
Good: Configure Jest
# package.json
{
"jest": {
"testEnvironment": "node",
"collectCoverage": true,
"coverageDirectory": "coverage",
"testMatch": ["**/__tests__/**/*.js", "**/?(*.)+(spec|test).js"]
}
}
# โ
Good: Run tests
npm test
npm test -- --watch
npm test -- --coverage
Jest Tests
// โ
Good: Basic test
describe('Math', () => {
test('adds numbers correctly', () => {
expect(2 + 2).toBe(4);
});
test('subtracts numbers correctly', () => {
expect(5 - 3).toBe(2);
});
});
// โ
Good: Test with setup and teardown
describe('Database', () => {
let db;
beforeAll(async () => {
db = await connectDB();
});
afterAll(async () => {
await db.close();
});
beforeEach(async () => {
await db.clear();
});
test('saves user', async () => {
const user = await db.saveUser({ name: 'John' });
expect(user.name).toBe('John');
});
});
// โ
Good: Mocking
jest.mock('../services/userService');
describe('UserController', () => {
test('gets user', async () => {
userService.getUser.mockResolvedValue({ id: 1, name: 'John' });
const user = await userController.getUser(1);
expect(user.name).toBe('John');
expect(userService.getUser).toHaveBeenCalledWith(1);
});
});
// โ
Good: Spying
describe('Logger', () => {
test('logs message', () => {
const spy = jest.spyOn(console, 'log');
logger.log('test');
expect(spy).toHaveBeenCalledWith('test');
spy.mockRestore();
});
});
Mocha Testing Framework
Mocha Setup
# โ
Good: Install Mocha
npm install --save-dev mocha chai
# โ
Good: Configure Mocha
# .mocharc.json
{
"require": "test/setup.js",
"spec": "test/**/*.test.js",
"timeout": 5000
}
# โ
Good: Run tests
npm test
npm test -- --watch
npm test -- --reporter json
Mocha Tests
const { expect } = require('chai');
// โ
Good: Basic test
describe('Math', () => {
it('adds numbers correctly', () => {
expect(2 + 2).to.equal(4);
});
it('subtracts numbers correctly', () => {
expect(5 - 3).to.equal(2);
});
});
// โ
Good: Async tests
describe('Database', () => {
let db;
before(async () => {
db = await connectDB();
});
after(async () => {
await db.close();
});
beforeEach(async () => {
await db.clear();
});
it('saves user', async () => {
const user = await db.saveUser({ name: 'John' });
expect(user.name).to.equal('John');
});
});
// โ
Good: Hooks
describe('API', () => {
let server;
before(() => {
server = startServer();
});
after(() => {
server.close();
});
it('returns users', async () => {
const response = await fetch('http://localhost:3000/api/users');
expect(response.status).to.equal(200);
});
});
Vitest Testing Framework
Vitest Setup
# โ
Good: Install Vitest
npm install --save-dev vitest
# โ
Good: Configure Vitest
# vitest.config.js
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html']
}
}
});
# โ
Good: Run tests
npm test
npm test -- --watch
npm test -- --coverage
Vitest Tests
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
// โ
Good: Basic test
describe('Math', () => {
it('adds numbers correctly', () => {
expect(2 + 2).toBe(4);
});
it('subtracts numbers correctly', () => {
expect(5 - 3).toBe(2);
});
});
// โ
Good: Mocking with Vitest
vi.mock('../services/userService');
describe('UserController', () => {
it('gets user', async () => {
const { userService } = await import('../services/userService');
userService.getUser.mockResolvedValue({ id: 1, name: 'John' });
const user = await userController.getUser(1);
expect(user.name).toBe('John');
expect(userService.getUser).toHaveBeenCalledWith(1);
});
});
// โ
Good: Snapshot testing
describe('Component', () => {
it('renders correctly', () => {
const component = render(<Button>Click me</Button>);
expect(component).toMatchSnapshot();
});
});
Testing Best Practices
Test Structure
// โ
Good: AAA pattern (Arrange, Act, Assert)
describe('UserService', () => {
it('creates user with valid data', () => {
// Arrange
const userData = { name: 'John', email: '[email protected]' };
// Act
const user = userService.createUser(userData);
// Assert
expect(user.name).toBe('John');
expect(user.email).toBe('[email protected]');
});
});
// โ
Good: Descriptive test names
describe('UserService', () => {
it('should create user with valid data', () => {
// Test
});
it('should throw error with invalid email', () => {
// Test
});
it('should update user name', () => {
// Test
});
});
// โ
Good: Test one thing per test
describe('UserService', () => {
it('creates user', () => {
const user = userService.createUser({ name: 'John' });
expect(user.name).toBe('John');
});
it('saves user to database', () => {
const user = userService.createUser({ name: 'John' });
expect(db.users).toContain(user);
});
});
Mocking and Stubbing
// โ
Good: Mock external dependencies
jest.mock('axios');
describe('UserAPI', () => {
it('fetches users', async () => {
axios.get.mockResolvedValue({
data: [{ id: 1, name: 'John' }]
});
const users = await userAPI.getUsers();
expect(users).toHaveLength(1);
expect(axios.get).toHaveBeenCalledWith('/api/users');
});
});
// โ
Good: Stub functions
describe('Logger', () => {
it('logs message', () => {
const logSpy = jest.fn();
const logger = new Logger(logSpy);
logger.log('test');
expect(logSpy).toHaveBeenCalledWith('test');
});
});
Test Coverage
Coverage Configuration
// โ
Good: Coverage thresholds
// jest.config.js
module.exports = {
collectCoverageFrom: [
'src/**/*.js',
'!src/index.js',
'!src/**/*.test.js'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
// โ
Good: Generate coverage report
npm test -- --coverage
// โ
Good: View coverage report
open coverage/lcov-report/index.html
Best Practices
-
Write testable code:
// โ Good: Testable code function calculateTotal(items) { return items.reduce((sum, item) => sum + item.price, 0); } // โ Bad: Hard to test function calculateTotal() { const items = getItemsFromDatabase(); return items.reduce((sum, item) => sum + item.price, 0); } -
Test behavior, not implementation:
// โ Good: Test behavior expect(userService.createUser(data)).toHaveProperty('id'); // โ Bad: Test implementation expect(userService.createUser).toHaveBeenCalled(); -
Keep tests simple:
// โ Good: Simple test it('adds numbers', () => { expect(add(2, 3)).toBe(5); }); // โ Bad: Complex test it('does everything', () => { // Multiple assertions // Multiple setups // Hard to understand });
Summary
Unit testing is essential. Key takeaways:
- Choose appropriate framework
- Write testable code
- Follow AAA pattern
- Mock external dependencies
- Test behavior
- Maintain coverage
- Keep tests simple
- Test one thing per test
Related Resources
- Jest Documentation
- Mocha Documentation
- Vitest Documentation
- Testing Best Practices
- Test Driven Development
Next Steps
- Learn about Integration Testing
- Explore E2E Testing
- Study Test Coverage
- Practice unit testing
- Write comprehensive tests
Comments