Introduction
JavaScript's dynamic nature and ecosystem present unique challenges and opportunities for Test-Driven Development. This article explores modern TDD practices in JavaScript.
Setting Up a TDD Environment
// package.json
{
"scripts": {
"test": "jest --watch",
"test:coverage": "jest --coverage"
},
"devDependencies": {
"jest": "^29.0.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0"
}
}
// jest.config.js
module.exports = {
testEnvironment: 'node',
collectCoverageFrom: ['src/**/*.js'],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
Writing Tests with Jest
// user.test.js
describe('User', () => {
test('should create user with valid data', () => {
const userData = {
name: 'John Doe',
email: 'john@example.com'
};
const user = new User(userData);
expect(user.name).toBe(userData.name);
expect(user.email).toBe(userData.email);
});
test('should validate email format', () => {
const userData = {
name: 'John Doe',
email: 'invalid-email'
};
expect(() => new User(userData))
.toThrow('Invalid email format');
});
});
// user.js
class User {
constructor({ name, email }) {
this.name = name;
this.email = email;
this.validateEmail();
}
validateEmail() {
const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
if (!emailRegex.test(this.email)) {
throw new Error('Invalid email format');
}
}
}
Async Testing
// userService.test.js
describe('UserService', () => {
test('should fetch user data', async () => {
const mockUser = { id: 1, name: 'John' };
const mockFetch = jest.fn().mockResolvedValue({
json: () => Promise.resolve(mockUser)
});
global.fetch = mockFetch;
const userService = new UserService();
const user = await userService.fetchUser(1);
expect(user).toEqual(mockUser);
expect(mockFetch).toHaveBeenCalledWith('/api/users/1');
});
test('should handle fetch errors', async () => {
const mockFetch = jest.fn().mockRejectedValue(
new Error('Network error')
);
global.fetch = mockFetch;
const userService = new UserService();
await expect(userService.fetchUser(1))
.rejects.toThrow('Network error');
});
});
Mocking Dependencies
// orderService.test.js
describe('OrderService', () => {
test('should process order', async () => {
const mockPaymentGateway = {
processPayment: jest.fn().mockResolvedValue({
success: true,
transactionId: '123'
})
};
const orderService = new OrderService(mockPaymentGateway);
const order = await orderService.process({
amount: 100,
paymentMethod: 'credit-card'
});
expect(order.status).toBe('paid');
expect(mockPaymentGateway.processPayment)
.toHaveBeenCalledWith({
amount: 100,
paymentMethod: 'credit-card'
});
});
});
Best Practices
- Use descriptive test names
- Follow the AAA pattern (Arrange, Act, Assert)
- Mock external dependencies
- Test edge cases and error conditions
- Keep tests focused and atomic
Conclusion
TDD in JavaScript requires understanding both the language's unique characteristics and modern testing tools. By following these practices, you can create robust and maintainable JavaScript applications.
"The key to successful TDD in JavaScript is understanding the ecosystem and choosing the right tools for your project." - Addy Osmani
Member discussion: