Testing & Tooling
Testing & Tooling
Section titled “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.
Testing Fundamentals
Section titled “Testing Fundamentals”Types of Testing
Section titled “Types of Testing”// Unit Testing - Testing individual functions/componentsfunction 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 togetherclass 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
Test-Driven Development (TDD)
Section titled “Test-Driven Development (TDD)”// 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)
Jest Testing Framework
Section titled “Jest Testing Framework”Basic Jest Setup
Section titled “Basic Jest Setup”{ "scripts": { "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage" }, "jest": { "testEnvironment": "node", "collectCoverageFrom": [ "src/**/*.js", "!src/index.js" ] }}
// jest.config.jsmodule.exports = { testEnvironment: 'jsdom', // for DOM testing setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'], transform: { '^.+\\.jsx?$': 'babel-jest' }, moduleNameMapping: { '\\.(css|less|scss)$': 'identity-obj-proxy' }};
Jest Testing Patterns
Section titled “Jest Testing Patterns”// Basic test structuredescribe('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 testingdescribe('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'); });});
// Mockingjest.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);});
Advanced Jest Features
Section titled “Advanced Jest Features”// Custom matchersexpect.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 testingtest('renders user profile correctly', () => { const userProfile = renderUserProfile({ name: 'John Doe', email: 'john@example.com', avatar: 'avatar.jpg' });
expect(userProfile).toMatchSnapshot();});
// Parameterized testsdescribe.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 testingtest('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);});
React Testing Library
Section titled “React Testing Library”Component Testing Best Practices
Section titled “Component Testing Best Practices”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 hooksimport { 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 End-to-End Testing
Section titled “Playwright End-to-End Testing”Setting Up Playwright
Section titled “Setting Up Playwright”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 examplesimport { 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'); });});
Development Tools
Section titled “Development Tools”ESLint Configuration
Section titled “ESLint Configuration”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 rulesconst 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', }); } }, }; },};
Prettier Configuration
Section titled “Prettier Configuration”module.exports = { semi: true, singleQuote: true, tabWidth: 2, trailingComma: 'es5', printWidth: 80, bracketSpacing: true, arrowParens: 'avoid', endOfLine: 'lf', overrides: [ { files: '*.json', options: { printWidth: 200, }, }, ],};
// .prettierignoredist/build/node_modules/coverage/*.min.js
Webpack Configuration
Section titled “Webpack Configuration”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', }, }, };};
Package.json Scripts
Section titled “Package.json Scripts”{ "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" } }}
CI/CD Pipeline
Section titled “CI/CD Pipeline”GitHub Actions Workflow
Section titled “GitHub Actions Workflow”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..."
Performance Testing
Section titled “Performance Testing”Lighthouse CI
Section titled “Lighthouse CI”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 budgetconst performanceBudget = { 'first-contentful-paint': 2000, 'largest-contentful-paint': 4000, 'cumulative-layout-shift': 0.1, 'total-blocking-time': 300,};
Bundle Analysis
Section titled “Bundle Analysis”// webpack-bundle-analyzer setupconst 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.