Skip to content

Testing & Tooling

Professional JavaScript development requires robust testing strategies and efficient development tools. This comprehensive guide covers testing frameworks, development workflows, and essential tooling for modern JavaScript applications.

// Unit Testing - Testing individual functions/components
function calculateTax(price, rate) {
if (price < 0 || rate < 0) {
throw new Error('Price and rate must be positive');
}
return price * rate;
}
// Integration Testing - Testing how parts work together
class PaymentService {
constructor(taxCalculator, paymentProcessor) {
this.taxCalculator = taxCalculator;
this.paymentProcessor = paymentProcessor;
}
async processOrder(order) {
const tax = this.taxCalculator.calculate(order.subtotal, order.taxRate);
const total = order.subtotal + tax;
return await this.paymentProcessor.charge({
amount: total,
customerId: order.customerId
});
}
}
// End-to-End Testing - Testing complete user workflows
// Example: User registration, login, and making a purchase
// 1. Write the test first (Red)
describe('User registration', () => {
test('should create user with valid email and password', () => {
const userData = {
email: 'test@example.com',
password: 'securePassword123'
};
const user = createUser(userData);
expect(user.email).toBe('test@example.com');
expect(user.id).toBeDefined();
expect(user.createdAt).toBeInstanceOf(Date);
});
});
// 2. Write minimal code to make test pass (Green)
function createUser({ email, password }) {
if (!email || !password) {
throw new Error('Email and password are required');
}
if (!isValidEmail(email)) {
throw new Error('Invalid email format');
}
if (password.length < 8) {
throw new Error('Password must be at least 8 characters');
}
return {
id: generateId(),
email,
password: hashPassword(password),
createdAt: new Date()
};
}
// 3. Refactor while keeping tests green (Refactor)
package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"jest": {
"testEnvironment": "node",
"collectCoverageFrom": [
"src/**/*.js",
"!src/index.js"
]
}
}
// jest.config.js
module.exports = {
testEnvironment: 'jsdom', // for DOM testing
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
transform: {
'^.+\\.jsx?$': 'babel-jest'
},
moduleNameMapping: {
'\\.(css|less|scss)$': 'identity-obj-proxy'
}
};
// Basic test structure
describe('Calculator', () => {
let calculator;
beforeEach(() => {
calculator = new Calculator();
});
afterEach(() => {
calculator.clear();
});
test('should add two numbers correctly', () => {
const result = calculator.add(2, 3);
expect(result).toBe(5);
});
test('should handle division by zero', () => {
expect(() => {
calculator.divide(10, 0);
}).toThrow('Division by zero');
});
test('should reset calculator state', () => {
calculator.add(5, 5);
calculator.clear();
expect(calculator.getCurrentValue()).toBe(0);
});
});
// Async testing
describe('API Service', () => {
test('should fetch user data', async () => {
const userData = await fetchUser(123);
expect(userData).toEqual({
id: 123,
name: expect.any(String),
email: expect.stringMatching(/\S+@\S+\.\S+/)
});
});
test('should handle API errors', async () => {
await expect(fetchUser('invalid-id')).rejects.toThrow('User not found');
});
});
// Mocking
jest.mock('./userService');
test('should call user service with correct parameters', () => {
const mockGetUser = jest.fn().mockResolvedValue({ id: 1, name: 'John' });
userService.getUser = mockGetUser;
const userController = new UserController(userService);
userController.getUserById(1);
expect(mockGetUser).toHaveBeenCalledWith(1);
expect(mockGetUser).toHaveBeenCalledTimes(1);
});
// Custom matchers
expect.extend({
toBeValidEmail(received) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const pass = emailRegex.test(received);
return {
message: () =>
`expected ${received} ${pass ? 'not ' : ''}to be a valid email`,
pass,
};
},
});
// Snapshot testing
test('renders user profile correctly', () => {
const userProfile = renderUserProfile({
name: 'John Doe',
email: 'john@example.com',
avatar: 'avatar.jpg'
});
expect(userProfile).toMatchSnapshot();
});
// Parameterized tests
describe.each([
[1, 1, 2],
[2, 2, 4],
[3, 3, 6],
])('Calculator.add(%i, %i)', (a, b, expected) => {
test(`returns ${expected}`, () => {
expect(Calculator.add(a, b)).toBe(expected);
});
});
// Performance testing
test('should process large dataset efficiently', () => {
const largeData = generateLargeDataset(10000);
const start = performance.now();
const result = processData(largeData);
const end = performance.now();
expect(end - start).toBeLessThan(1000); // Should complete in under 1 second
expect(result).toHaveLength(10000);
});
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserProfile } from './UserProfile';
describe('UserProfile', () => {
const mockUser = {
id: 1,
name: 'John Doe',
email: 'john@example.com',
avatar: 'avatar.jpg'
};
test('renders user information', () => {
render(<UserProfile user={mockUser} />);
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
expect(screen.getByAltText('User avatar')).toHaveAttribute('src', 'avatar.jpg');
});
test('handles edit button click', async () => {
const onEdit = jest.fn();
const user = userEvent.setup();
render(<UserProfile user={mockUser} onEdit={onEdit} />);
const editButton = screen.getByRole('button', { name: /edit/i });
await user.click(editButton);
expect(onEdit).toHaveBeenCalledWith(mockUser.id);
});
test('shows loading state while saving', async () => {
const onSave = jest.fn().mockResolvedValue();
render(<UserProfile user={mockUser} onSave={onSave} />);
const saveButton = screen.getByRole('button', { name: /save/i });
fireEvent.click(saveButton);
expect(screen.getByText('Saving...')).toBeInTheDocument();
await waitFor(() => {
expect(screen.queryByText('Saving...')).not.toBeInTheDocument();
});
});
});
// Testing hooks
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
test('should increment counter', () => {
const { result } = renderHook(() => useCounter(0));
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('should reset counter', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(0);
});
});
playwright.config.js
module.exports = {
testDir: './e2e',
timeout: 30000,
retries: 2,
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
],
};
// E2E test examples
import { test, expect } from '@playwright/test';
test.describe('User Authentication', () => {
test('should login with valid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'user@example.com');
await page.fill('[data-testid="password"]', 'password123');
await page.click('[data-testid="login-button"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid="welcome-message"]')).toContainText('Welcome back');
});
test('should show error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'invalid@example.com');
await page.fill('[data-testid="password"]', 'wrongpassword');
await page.click('[data-testid="login-button"]');
await expect(page.locator('[data-testid="error-message"]')).toContainText('Invalid credentials');
});
});
test.describe('Shopping Cart', () => {
test('should add items to cart', async ({ page }) => {
await page.goto('/products');
// Add first product
await page.click('[data-testid="product-1"] [data-testid="add-to-cart"]');
await expect(page.locator('[data-testid="cart-count"]')).toContainText('1');
// Add second product
await page.click('[data-testid="product-2"] [data-testid="add-to-cart"]');
await expect(page.locator('[data-testid="cart-count"]')).toContainText('2');
// View cart
await page.click('[data-testid="cart-icon"]');
await expect(page.locator('[data-testid="cart-items"]')).toContainText('2 items');
});
});
.eslintrc.js
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
jest: true,
},
extends: [
'eslint:recommended',
'@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'prettier',
],
plugins: ['@typescript-eslint', 'react', 'react-hooks'],
rules: {
'no-console': 'warn',
'no-unused-vars': 'error',
'prefer-const': 'error',
'no-var': 'error',
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/explicit-function-return-type': 'warn',
},
settings: {
react: {
version: 'detect',
},
},
};
// Custom ESLint rules
const noConsoleInProduction = {
meta: {
type: 'problem',
docs: {
description: 'Disallow console statements in production',
},
},
create(context) {
return {
CallExpression(node) {
if (
node.callee.type === 'MemberExpression' &&
node.callee.object.name === 'console' &&
process.env.NODE_ENV === 'production'
) {
context.report({
node,
message: 'Console statements should not be used in production',
});
}
},
};
},
};
.prettierrc.js
module.exports = {
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: 'es5',
printWidth: 80,
bracketSpacing: true,
arrowParens: 'avoid',
endOfLine: 'lf',
overrides: [
{
files: '*.json',
options: {
printWidth: 200,
},
},
],
};
// .prettierignore
dist/
build/
node_modules/
coverage/
*.min.js
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: isProduction ? '[name].[contenthash].js' : '[name].js',
clean: true,
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
},
},
},
{
test: /\.css$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader',
'postcss-loader',
],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
...(isProduction ? [new MiniCssExtractPlugin()] : []),
],
devServer: {
contentBase: './dist',
hot: true,
open: true,
},
optimization: {
splitChunks: {
chunks: 'all',
},
},
};
};
{
"scripts": {
"dev": "webpack serve --mode development",
"build": "webpack --mode production",
"test": "jest",
"test:watch": "jest --watch",
"test:e2e": "playwright test",
"lint": "eslint src --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint src --ext .js,.jsx,.ts,.tsx --fix",
"format": "prettier --write src/**/*.{js,jsx,ts,tsx,json,css,md}",
"type-check": "tsc --noEmit",
"pre-commit": "lint-staged",
"prepare": "husky install"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write",
"git add"
],
"*.{json,css,md}": [
"prettier --write",
"git add"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"pre-push": "npm test"
}
}
}
.github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x, 20.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linting
run: npm run lint
- name: Run type checking
run: npm run type-check
- name: Run unit tests
run: npm run test -- --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
e2e:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '18.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install
- name: Build application
run: npm run build
- name: Start application
run: npm start &
- name: Wait for app to be ready
run: npx wait-on http://localhost:3000
- name: Run E2E tests
run: npm run test:e2e
- name: Upload test results
uses: actions/upload-artifact@v3
if: failure()
with:
name: playwright-report
path: playwright-report/
deploy:
runs-on: ubuntu-latest
needs: [test, e2e]
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Deploy to production
run: echo "Deploying to production..."
lighthouserc.js
module.exports = {
ci: {
collect: {
url: ['http://localhost:3000'],
startServerCommand: 'npm start',
},
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.9 }],
'categories:accessibility': ['error', { minScore: 0.9 }],
'categories:best-practices': ['error', { minScore: 0.9 }],
'categories:seo': ['error', { minScore: 0.9 }],
},
},
upload: {
target: 'lhci',
serverBaseUrl: 'https://your-lhci-server.com',
},
},
};
// Performance budget
const performanceBudget = {
'first-contentful-paint': 2000,
'largest-contentful-paint': 4000,
'cumulative-layout-shift': 0.1,
'total-blocking-time': 300,
};
// webpack-bundle-analyzer setup
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: process.env.ANALYZE ? 'server' : 'disabled',
}),
],
};
// Package.json script
{
"scripts": {
"analyze": "ANALYZE=true npm run build"
}
}

This comprehensive testing and tooling guide provides everything needed to set up a robust development workflow with modern JavaScript applications.


Implement these testing strategies and tools to build confidence in your code quality and deployment process.