JavaScript Testing and Quality Assurance
50 minTesting is crucial for maintaining code quality and preventing bugs in JavaScript applications.
Modern testing frameworks like Jest, Mocha, and Vitest provide comprehensive testing capabilities.
Test-driven development (TDD) and behavior-driven development (BDD) improve code design and reliability.
Code coverage, linting, and automated testing pipelines ensure consistent code quality.
Exercise
Create a comprehensive testing suite for a JavaScript application using Jest with mocking, testing utilities, and coverage reporting.
// JavaScript Testing Suite with Jest
// package.json dependencies
const dependencies = {
"jest": "^29.0.0",
"@testing-library/jest-dom": "^5.16.5",
"jest-environment-jsdom": "^29.0.0"
};
// Calculator class to test
class Calculator {
add(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Arguments must be numbers');
}
return a + b;
}
subtract(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Arguments must be numbers');
}
return a - b;
}
multiply(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Arguments must be numbers');
}
return a * b;
}
divide(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Arguments must be numbers');
}
if (b === 0) {
throw new Error('Division by zero is not allowed');
}
return a / b;
}
power(base, exponent) {
if (typeof base !== 'number' || typeof exponent !== 'number') {
throw new TypeError('Arguments must be numbers');
}
return Math.pow(base, exponent);
}
factorial(n) {
if (typeof n !== 'number') {
throw new TypeError('Argument must be a number');
}
if (n < 0) {
throw new Error('Factorial is not defined for negative numbers');
}
if (n === 0 || n === 1) {
return 1;
}
return n * this.factorial(n - 1);
}
}
// User service with async operations
class UserService {
constructor(apiClient) {
this.apiClient = apiClient;
this.cache = new Map();
}
async getUser(id) {
if (this.cache.has(id)) {
return this.cache.get(id);
}
try {
const user = await this.apiClient.fetchUser(id);
this.cache.set(id, user);
return user;
} catch (error) {
throw new Error(`Failed to fetch user ${id}: ${error.message}`);
}
}
async createUser(userData) {
try {
const user = await this.apiClient.createUser(userData);
this.cache.set(user.id, user);
return user;
} catch (error) {
throw new Error(`Failed to create user: ${error.message}`);
}
}
clearCache() {
this.cache.clear();
}
getCacheSize() {
return this.cache.size;
}
}
// Mock API client
class MockApiClient {
constructor() {
this.users = new Map();
this.nextId = 1;
}
async fetchUser(id) {
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 10));
const user = this.users.get(id);
if (!user) {
throw new Error('User not found');
}
return user;
}
async createUser(userData) {
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 10));
const user = {
id: this.nextId++,
...userData,
createdAt: new Date().toISOString()
};
this.users.set(user.id, user);
return user;
}
}
// Test suite for Calculator
describe('Calculator', () => {
let calculator;
beforeEach(() => {
calculator = new Calculator();
});
describe('add method', () => {
test('should add two positive numbers correctly', () => {
expect(calculator.add(2, 3)).toBe(5);
expect(calculator.add(0, 0)).toBe(0);
expect(calculator.add(-1, 1)).toBe(0);
});
test('should throw TypeError for non-numeric arguments', () => {
expect(() => calculator.add('2', 3)).toThrow(TypeError);
expect(() => calculator.add(2, '3')).toThrow(TypeError);
expect(() => calculator.add(null, 3)).toThrow(TypeError);
expect(() => calculator.add(2, undefined)).toThrow(TypeError);
});
});
describe('subtract method', () => {
test('should subtract two numbers correctly', () => {
expect(calculator.subtract(5, 3)).toBe(2);
expect(calculator.subtract(0, 0)).toBe(0);
expect(calculator.subtract(-1, -1)).toBe(0);
});
test('should throw TypeError for non-numeric arguments', () => {
expect(() => calculator.subtract('5', 3)).toThrow(TypeError);
expect(() => calculator.subtract(5, '3')).toThrow(TypeError);
});
});
describe('multiply method', () => {
test('should multiply two numbers correctly', () => {
expect(calculator.multiply(2, 3)).toBe(6);
expect(calculator.multiply(0, 5)).toBe(0);
expect(calculator.multiply(-2, 3)).toBe(-6);
});
test('should throw TypeError for non-numeric arguments', () => {
expect(() => calculator.multiply('2', 3)).toThrow(TypeError);
});
});
describe('divide method', () => {
test('should divide two numbers correctly', () => {
expect(calculator.divide(6, 2)).toBe(3);
expect(calculator.divide(5, 2)).toBe(2.5);
expect(calculator.divide(0, 5)).toBe(0);
});
test('should throw Error for division by zero', () => {
expect(() => calculator.divide(5, 0)).toThrow('Division by zero is not allowed');
});
test('should throw TypeError for non-numeric arguments', () => {
expect(() => calculator.divide('6', 2)).toThrow(TypeError);
});
});
describe('power method', () => {
test('should calculate power correctly', () => {
expect(calculator.power(2, 3)).toBe(8);
expect(calculator.power(5, 0)).toBe(1);
expect(calculator.power(2, -1)).toBe(0.5);
});
test('should throw TypeError for non-numeric arguments', () => {
expect(() => calculator.power('2', 3)).toThrow(TypeError);
});
});
describe('factorial method', () => {
test('should calculate factorial correctly', () => {
expect(calculator.factorial(0)).toBe(1);
expect(calculator.factorial(1)).toBe(1);
expect(calculator.factorial(5)).toBe(120);
});
test('should throw Error for negative numbers', () => {
expect(() => calculator.factorial(-1)).toThrow('Factorial is not defined for negative numbers');
});
test('should throw TypeError for non-numeric arguments', () => {
expect(() => calculator.factorial('5')).toThrow(TypeError);
});
});
});
// Test suite for UserService
describe('UserService', () => {
let userService;
let mockApiClient;
beforeEach(() => {
mockApiClient = new MockApiClient();
userService = new UserService(mockApiClient);
});
afterEach(() => {
userService.clearCache();
});
describe('getUser method', () => {
test('should fetch user from API and cache it', async () => {
const userData = { name: 'John Doe', email: 'john@example.com' };
const user = await mockApiClient.createUser(userData);
const result = await userService.getUser(user.id);
expect(result).toEqual(user);
expect(userService.getCacheSize()).toBe(1);
});
test('should return cached user on subsequent calls', async () => {
const userData = { name: 'Jane Doe', email: 'jane@example.com' };
const user = await mockApiClient.createUser(userData);
// First call - should fetch from API
await userService.getUser(user.id);
const firstCallCacheSize = userService.getCacheSize();
// Second call - should use cache
await userService.getUser(user.id);
const secondCallCacheSize = userService.getCacheSize();
expect(firstCallCacheSize).toBe(1);
expect(secondCallCacheSize).toBe(1); // Cache size should remain the same
});
test('should throw error for non-existent user', async () => {
await expect(userService.getUser(999)).rejects.toThrow('Failed to fetch user 999: User not found');
});
});
describe('createUser method', () => {
test('should create user and cache it', async () => {
const userData = { name: 'New User', email: 'new@example.com' };
const result = await userService.createUser(userData);
expect(result).toHaveProperty('id');
expect(result.name).toBe(userData.name);
expect(result.email).toBe(userData.email);
expect(result).toHaveProperty('createdAt');
expect(userService.getCacheSize()).toBe(1);
});
});
describe('cache management', () => {
test('should clear cache correctly', async () => {
const userData = { name: 'Test User', email: 'test@example.com' };
const user = await userService.createUser(userData);
expect(userService.getCacheSize()).toBe(1);
userService.clearCache();
expect(userService.getCacheSize()).toBe(0);
});
});
});
// Test utilities and helpers
describe('Test Utilities', () => {
test('should use test.each for parameterized testing', () => {
const testCases = [
[1, 2, 3],
[0, 0, 0],
[-1, 1, 0],
[100, 200, 300]
];
test.each(testCases)('add(%i, %i) should return %i', (a, b, expected) => {
const calculator = new Calculator();
expect(calculator.add(a, b)).toBe(expected);
});
});
test('should use test.todo for planned tests', () => {
test.todo('should implement square root method');
test.todo('should implement percentage calculation');
test.todo('should add unit conversion methods');
});
test('should use test.skip for temporarily disabled tests', () => {
test.skip('this test is temporarily disabled', () => {
expect(true).toBe(false);
});
});
});
// Performance testing
describe('Performance Tests', () => {
test('should complete factorial calculation within reasonable time', () => {
const startTime = performance.now();
const calculator = new Calculator();
const result = calculator.factorial(10);
const endTime = performance.now();
const executionTime = endTime - startTime;
expect(result).toBe(3628800);
expect(executionTime).toBeLessThan(100); // Should complete within 100ms
});
});
// Integration tests
describe('Integration Tests', () => {
test('should perform complex calculation chain', () => {
const calculator = new Calculator();
// Complex calculation: (2 + 3) * 4 / 2 ^ 2
const result = calculator.divide(
calculator.multiply(
calculator.add(2, 3),
4
),
calculator.power(2, 2)
);
expect(result).toBe(5); // (5 * 4) / 4 = 5
});
});
// Mock and spy testing
describe('Mock and Spy Testing', () => {
test('should spy on method calls', () => {
const calculator = new Calculator();
const addSpy = jest.spyOn(calculator, 'add');
calculator.add(2, 3);
expect(addSpy).toHaveBeenCalledWith(2, 3);
expect(addSpy).toHaveBeenCalledTimes(1);
addSpy.mockRestore();
});
test('should mock external dependencies', () => {
const mockApiClient = {
fetchUser: jest.fn().mockResolvedValue({ id: 1, name: 'Mock User' }),
createUser: jest.fn().mockResolvedValue({ id: 2, name: 'New Mock User' })
};
const userService = new UserService(mockApiClient);
expect(mockApiClient.fetchUser).not.toHaveBeenCalled();
expect(mockApiClient.createUser).not.toHaveBeenCalled();
});
});
// Jest configuration example
const jestConfig = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
collectCoverageFrom: [
'src/**/*.{js,jsx}',
'!src/**/*.test.{js,jsx}',
'!src/index.js'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
testMatch: [
'<rootDir>/src/**/__tests__/**/*.{js,jsx}',
'<rootDir>/src/**/*.{test,spec}.{js,jsx}'
]
};
// Setup file example (src/setupTests.js)
const setupTests = `
import '@testing-library/jest-dom';
// Global test setup
beforeAll(() => {
console.log('Setting up test environment...');
});
afterAll(() => {
console.log('Cleaning up test environment...');
});
// Custom matchers
expect.extend({
toBeWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling;
if (pass) {
return {
message: () => `expected ${received} not to be within range ${floor} - ${ceiling}`,
pass: true,
};
} else {
return {
message: () => `expected ${received} to be within range ${floor} - ${ceiling}`,
pass: false,
};
}
},
});
// Global test utilities
global.testUtils = {
createMockUser: (overrides = {}) => ({
id: 1,
name: 'Test User',
email: 'test@example.com',
...overrides
}),
waitForElement: (selector) => {
return new Promise((resolve) => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver(() => {
if (document.querySelector(selector)) {
resolve(document.querySelector(selector));
observer.disconnect();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}
};
`;
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
Calculator,
UserService,
MockApiClient,
jestConfig,
setupTests
};
} else if (typeof window !== 'undefined') {
window.Calculator = Calculator;
window.UserService = UserService;
window.MockApiClient = MockApiClient;
}
// Run tests if in Node.js environment
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
console.log('Running tests in test environment...');
}