javascript-testing-patterns
upstream

Implement comprehensive testing strategies using Jest, Vitest, and Testing Library for unit tests, integration tests, and end-to-end testing with mocking, fixtures, and test-driven development. Use when writing JavaScript/TypeScript tests, setting up test infrastructure, or implementing TDD/BDD workflows.

ID: javascript-testing-patterns
Preview
Codigo

JavaScript Testing Patterns


Comprehensive guide for implementing robust testing strategies in JavaScript/TypeScript applications using modern testing frameworks and best practices.


When to Use This Skill


  • Setting up test infrastructure for new projects
  • Writing unit tests for functions and classes
  • Creating integration tests for APIs and services
  • Implementing end-to-end tests for user flows
  • Mocking external dependencies and APIs
  • Testing React, Vue, or other frontend components
  • Implementing test-driven development (TDD)
  • Setting up continuous testing in CI/CD pipelines

Testing Frameworks


Jest - Full-Featured Testing Framework


Setup:

``typescript

// jest.config.ts

import type { Config } from 'jest';


const config: Config = {

preset: 'ts-jest',

testEnvironment: 'node',

roots: ['<rootDir>/src'],

testMatch: ['/__tests__//*.ts', '**/?(*.)+(spec|test).ts'],

collectCoverageFrom: [

'src/**/*.ts',

'!src/**/*.d.ts',

'!src/**/*.interface.ts',

],

coverageThreshold: {

global: {

branches: 80,

functions: 80,

lines: 80,

statements: 80,

},

},

setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts'],

};


export default config;

`


Vitest - Fast, Vite-Native Testing


Setup:

`typescript

// vitest.config.ts

import { defineConfig } from 'vitest/config';


export default defineConfig({

test: {

globals: true,

environment: 'node',

coverage: {

provider: 'v8',

reporter: ['text', 'json', 'html'],

exclude: ['**/*.d.ts', '**/*.config.ts', '/dist/'],

},

setupFiles: ['./src/test/setup.ts'],

},

});

`


Unit Testing Patterns


Pattern 1: Testing Pure Functions


`typescript

// utils/calculator.ts

export function add(a: number, b: number): number {

return a + b;

}


export function divide(a: number, b: number): number {

if (b === 0) {

throw new Error('Division by zero');

}

return a / b;

}


// utils/calculator.test.ts

import { describe, it, expect } from 'vitest';

import { add, divide } from './calculator';


describe('Calculator', () => {

describe('add', () => {

it('should add two positive numbers', () => {

expect(add(2, 3)).toBe(5);

});


it('should add negative numbers', () => {

expect(add(-2, -3)).toBe(-5);

});


it('should handle zero', () => {

expect(add(0, 5)).toBe(5);

expect(add(5, 0)).toBe(5);

});

});


describe('divide', () => {

it('should divide two numbers', () => {

expect(divide(10, 2)).toBe(5);

});


it('should handle decimal results', () => {

expect(divide(5, 2)).toBe(2.5);

});


it('should throw error when dividing by zero', () => {

expect(() => divide(10, 0)).toThrow('Division by zero');

});

});

});

`


Pattern 2: Testing Classes


`typescript

// services/user.service.ts

export class UserService {

private users: Map<string, User> = new Map();


create(user: User): User {

if (this.users.has(user.id)) {

throw new Error('User already exists');

}

this.users.set(user.id, user);

return user;

}


findById(id: string): User | undefined {

return this.users.get(id);

}


update(id: string, updates: Partial<User>): User {

const user = this.users.get(id);

if (!user) {

throw new Error('User not found');

}

const updated = { ...user, ...updates };

this.users.set(id, updated);

return updated;

}


delete(id: string): boolean {

return this.users.delete(id);

}

}


// services/user.service.test.ts

import { describe, it, expect, beforeEach } from 'vitest';

import { UserService } from './user.service';


describe('UserService', () => {

let service: UserService;


beforeEach(() => {

service = new UserService();

});


describe('create', () => {

it('should create a new user', () => {

const user = { id: '1', name: 'John', email: 'john@example.com' };

const created = service.create(user);


expect(created).toEqual(user);

expect(service.findById('1')).toEqual(user);

});


it('should throw error if user already exists', () => {

const user = { id: '1', name: 'John', email: 'john@example.com' };

service.create(user);


expect(() => service.create(user)).toThrow('User already exists');

});

});


describe('update', () => {

it('should update existing user', () => {

const user = { id: '1', name: 'John', email: 'john@example.com' };

service.create(user);


const updated = service.update('1', { name: 'Jane' });


expect(updated.name).toBe('Jane');

expect(updated.email).toBe('john@example.com');

});


it('should throw error if user not found', () => {

expect(() => service.update('999', { name: 'Jane' }))

.toThrow('User not found');

});

});

});

`


Pattern 3: Testing Async Functions


`typescript

// services/api.service.ts

export class ApiService {

async fetchUser(id: string): Promise<User> {

const response = await fetch(https://api.example.com/users/${id});

if (!response.ok) {

throw new Error('User not found');

}

return response.json();

}


async createUser(user: CreateUserDTO): Promise<User> {

const response = await fetch('https://api.example.com/users', {

method: 'POST',

headers: { 'Content-Type': 'application/json' },

body: JSON.stringify(user),

});

return response.json();

}

}


// services/api.service.test.ts

import { describe, it, expect, vi, beforeEach } from 'vitest';

import { ApiService } from './api.service';


// Mock fetch globally

global.fetch = vi.fn();


describe('ApiService', () => {

let service: ApiService;


beforeEach(() => {

service = new ApiService();

vi.clearAllMocks();

});


describe('fetchUser', () => {

it('should fetch user successfully', async () => {

const mockUser = { id: '1', name: 'John', email: 'john@example.com' };


(fetch as any).mockResolvedValueOnce({

ok: true,

json: async () => mockUser,

});


const user = await service.fetchUser('1');


expect(user).toEqual(mockUser);

expect(fetch).toHaveBeenCalledWith('https://api.example.com/users/1');

});


it('should throw error if user not found', async () => {

(fetch as any).mockResolvedValueOnce({

ok: false,

});


await expect(service.fetchUser('999')).rejects.toThrow('User not found');

});

});


describe('createUser', () => {

it('should create user successfully', async () => {

const newUser = { name: 'John', email: 'john@example.com' };

const createdUser = { id: '1', ...newUser };


(fetch as any).mockResolvedValueOnce({

ok: true,

json: async () => createdUser,

});


const user = await service.createUser(newUser);


expect(user).toEqual(createdUser);

expect(fetch).toHaveBeenCalledWith(

'https://api.example.com/users',

expect.objectContaining({

method: 'POST',

body: JSON.stringify(newUser),

})

);

});

});

});

`


Mocking Patterns


Pattern 1: Mocking Modules


`typescript

// services/email.service.ts

import nodemailer from 'nodemailer';


export class EmailService {

private transporter = nodemailer.createTransport({

host: process.env.SMTP_HOST,

port: 587,

auth: {

user: process.env.SMTP_USER,

pass: process.env.SMTP_PASS,

},

});


async sendEmail(to: string, subject: string, html: string) {

await this.transporter.sendMail({

from: process.env.EMAIL_FROM,

to,

subject,

html,

});

}

}


// services/email.service.test.ts

import { describe, it, expect, vi, beforeEach } from 'vitest';

import { EmailService } from './email.service';


vi.mock('nodemailer', () => ({

default: {

createTransport: vi.fn(() => ({

sendMail: vi.fn().mockResolvedValue({ messageId: '123' }),

})),

},

}));


describe('EmailService', () => {

let service: EmailService;


beforeEach(() => {

service = new EmailService();

});


it('should send email successfully', async () => {

await service.sendEmail(

'test@example.com',

'Test Subject',

'<p>Test Body</p>'

);


expect(service['transporter'].sendMail).toHaveBeenCalledWith(

expect.objectContaining({

to: 'test@example.com',

subject: 'Test Subject',

})

);

});

});

`


Pattern 2: Dependency Injection for Testing


`typescript

// services/user.service.ts

export interface IUserRepository {

findById(id: string): Promise<User | null>;

create(user: User): Promise<User>;

}


export class UserService {

constructor(private userRepository: IUserRepository) {}


async getUser(id: string): Promise<User> {

const user = await this.userRepository.findById(id);

if (!user) {

throw new Error('User not found');

}

return user;

}


async createUser(userData: CreateUserDTO): Promise<User> {

// Business logic here

const user = { id: generateId(), ...userData };

return this.userRepository.create(user);

}

}


// services/user.service.test.ts

import { describe, it, expect, vi, beforeEach } from 'vitest';

import { UserService, IUserRepository } from './user.service';


describe('UserService', () => {

let service: UserService;

let mockRepository: IUserRepository;


beforeEach(() => {

mockRepository = {

findById: vi.fn(),

create: vi.fn(),

};

service = new UserService(mockRepository);

});


describe('getUser', () => {

it('should return user if found', async () => {

const mockUser = { id: '1', name: 'John', email: 'john@example.com' };

vi.mocked(mockRepository.findById).mockResolvedValue(mockUser);


const user = await service.getUser('1');


expect(user).toEqual(mockUser);

expect(mockRepository.findById).toHaveBeenCalledWith('1');

});


it('should throw error if user not found', async () => {

vi.mocked(mockRepository.findById).mockResolvedValue(null);


await expect(service.getUser('999')).rejects.toThrow('User not found');

});

});


describe('createUser', () => {

it('should create user successfully', async () => {

const userData = { name: 'John', email: 'john@example.com' };

const createdUser = { id: '1', ...userData };


vi.mocked(mockRepository.create).mockResolvedValue(createdUser);


const user = await service.createUser(userData);


expect(user).toEqual(createdUser);

expect(mockRepository.create).toHaveBeenCalled();

});

});

});

`


Pattern 3: Spying on Functions


`typescript

// utils/logger.ts

export const logger = {

info: (message: string) => console.log(INFO: ${message}),

error: (message: string) => console.error(ERROR: ${message}),

};


// services/order.service.ts

import { logger } from '../utils/logger';


export class OrderService {

async processOrder(orderId: string): Promise<void> {

logger.info(Processing order ${orderId});

// Process order logic

logger.info(Order ${orderId} processed successfully);

}

}


// services/order.service.test.ts

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

import { OrderService } from './order.service';

import { logger } from '../utils/logger';


describe('OrderService', () => {

let service: OrderService;

let loggerSpy: any;


beforeEach(() => {

service = new OrderService();

loggerSpy = vi.spyOn(logger, 'info');

});


afterEach(() => {

loggerSpy.mockRestore();

});


it('should log order processing', async () => {

await service.processOrder('123');


expect(loggerSpy).toHaveBeenCalledWith('Processing order 123');

expect(loggerSpy).toHaveBeenCalledWith('Order 123 processed successfully');

expect(loggerSpy).toHaveBeenCalledTimes(2);

});

});

`


Integration Testing


Pattern 1: API Integration Tests


`typescript

// tests/integration/user.api.test.ts

import request from 'supertest';

import { app } from '../../src/app';

import { pool } from '../../src/config/database';


describe('User API Integration Tests', () => {

beforeAll(async () => {

// Setup test database

await pool.query('CREATE TABLE IF NOT EXISTS users (...)');

});


afterAll(async () => {

// Cleanup

await pool.query('DROP TABLE IF EXISTS users');

await pool.end();

});


beforeEach(async () => {

// Clear data before each test

await pool.query('TRUNCATE TABLE users CASCADE');

});


describe('POST /api/users', () => {

it('should create a new user', async () => {

const userData = {

name: 'John Doe',

email: 'john@example.com',

password: 'password123',

};


const response = await request(app)

.post('/api/users')

.send(userData)

.expect(201);


expect(response.body).toMatchObject({

name: userData.name,

email: userData.email,

});

expect(response.body).toHaveProperty('id');

expect(response.body).not.toHaveProperty('password');

});


it('should return 400 if email is invalid', async () => {

const userData = {

name: 'John Doe',

email: 'invalid-email',

password: 'password123',

};


const response = await request(app)

.post('/api/users')

.send(userData)

.expect(400);


expect(response.body).toHaveProperty('error');

});


it('should return 409 if email already exists', async () => {

const userData = {

name: 'John Doe',

email: 'john@example.com',

password: 'password123',

};


await request(app).post('/api/users').send(userData);


const response = await request(app)

.post('/api/users')

.send(userData)

.expect(409);


expect(response.body.error).toContain('already exists');

});

});


describe('GET /api/users/:id', () => {

it('should get user by id', async () => {

const createResponse = await request(app)

.post('/api/users')

.send({

name: 'John Doe',

email: 'john@example.com',

password: 'password123',

});


const userId = createResponse.body.id;


const response = await request(app)

.get(/api/users/${userId})

.expect(200);


expect(response.body).toMatchObject({

id: userId,

name: 'John Doe',

email: 'john@example.com',

});

});


it('should return 404 if user not found', async () => {

await request(app)

.get('/api/users/999')

.expect(404);

});

});


describe('Authentication', () => {

it('should require authentication for protected routes', async () => {

await request(app)

.get('/api/users/me')

.expect(401);

});


it('should allow access with valid token', async () => {

// Create user and login

await request(app)

.post('/api/users')

.send({

name: 'John Doe',

email: 'john@example.com',

password: 'password123',

});


const loginResponse = await request(app)

.post('/api/auth/login')

.send({

email: 'john@example.com',

password: 'password123',

});


const token = loginResponse.body.token;


const response = await request(app)

.get('/api/users/me')

.set('Authorization', Bearer ${token})

.expect(200);


expect(response.body.email).toBe('john@example.com');

});

});

});

`


Pattern 2: Database Integration Tests


`typescript

// tests/integration/user.repository.test.ts

import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';

import { Pool } from 'pg';

import { UserRepository } from '../../src/repositories/user.repository';


describe('UserRepository Integration Tests', () => {

let pool: Pool;

let repository: UserRepository;


beforeAll(async () => {

pool = new Pool({

host: 'localhost',

port: 5432,

database: 'test_db',

user: 'test_user',

password: 'test_password',

});


repository = new UserRepository(pool);


// Create tables

await pool.query(

CREATE TABLE IF NOT EXISTS users (

id SERIAL PRIMARY KEY,

name VARCHAR(255) NOT NULL,

email VARCHAR(255) UNIQUE NOT NULL,

password VARCHAR(255) NOT NULL,

created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP

)

);

});


afterAll(async () => {

await pool.query('DROP TABLE IF EXISTS users');

await pool.end();

});


beforeEach(async () => {

await pool.query('TRUNCATE TABLE users CASCADE');

});


it('should create a user', async () => {

const user = await repository.create({

name: 'John Doe',

email: 'john@example.com',

password: 'hashed_password',

});


expect(user).toHaveProperty('id');

expect(user.name).toBe('John Doe');

expect(user.email).toBe('john@example.com');

});


it('should find user by email', async () => {

await repository.create({

name: 'John Doe',

email: 'john@example.com',

password: 'hashed_password',

});


const user = await repository.findByEmail('john@example.com');


expect(user).toBeTruthy();

expect(user?.name).toBe('John Doe');

});


it('should return null if user not found', async () => {

const user = await repository.findByEmail('nonexistent@example.com');

expect(user).toBeNull();

});

});

`


Frontend Testing with Testing Library


Pattern 1: React Component Testing


`typescript

// components/UserForm.tsx

import { useState } from 'react';


interface Props {

onSubmit: (user: { name: string; email: string }) => void;

}


export function UserForm({ onSubmit }: Props) {

const [name, setName] = useState('');

const [email, setEmail] = useState('');


const handleSubmit = (e: React.FormEvent) => {

e.preventDefault();

onSubmit({ name, email });

};


return (

<form onSubmit={handleSubmit}>

<input

type="text"

placeholder="Name"

value={name}

onChange={(e) => setName(e.target.value)}

data-testid="name-input"

/>

<input

type="email"

placeholder="Email"

value={email}

onChange={(e) => setEmail(e.target.value)}

data-testid="email-input"

/>

<button type="submit">Submit</button>

</form>

);

}


// components/UserForm.test.tsx

import { render, screen, fireEvent } from '@testing-library/react';

import { describe, it, expect, vi } from 'vitest';

import { UserForm } from './UserForm';


describe('UserForm', () => {

it('should render form inputs', () => {

render(<UserForm onSubmit={vi.fn()} />);


expect(screen.getByPlaceholderText('Name')).toBeInTheDocument();

expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();

expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();

});


it('should update input values', () => {

render(<UserForm onSubmit={vi.fn()} />);


const nameInput = screen.getByTestId('name-input') as HTMLInputElement;

const emailInput = screen.getByTestId('email-input') as HTMLInputElement;


fireEvent.change(nameInput, { target: { value: 'John Doe' } });

fireEvent.change(emailInput, { target: { value: 'john@example.com' } });


expect(nameInput.value).toBe('John Doe');

expect(emailInput.value).toBe('john@example.com');

});


it('should call onSubmit with form data', () => {

const onSubmit = vi.fn();

render(<UserForm onSubmit={onSubmit} />);


fireEvent.change(screen.getByTestId('name-input'), {

target: { value: 'John Doe' },

});

fireEvent.change(screen.getByTestId('email-input'), {

target: { value: 'john@example.com' },

});

fireEvent.click(screen.getByRole('button', { name: 'Submit' }));


expect(onSubmit).toHaveBeenCalledWith({

name: 'John Doe',

email: 'john@example.com',

});

});

});

`


Pattern 2: Testing Hooks


`typescript

// hooks/useCounter.ts

import { useState, useCallback } from 'react';


export function useCounter(initialValue = 0) {

const [count, setCount] = useState(initialValue);


const increment = useCallback(() => setCount((c) => c + 1), []);

const decrement = useCallback(() => setCount((c) => c - 1), []);

const reset = useCallback(() => setCount(initialValue), [initialValue]);


return { count, increment, decrement, reset };

}


// hooks/useCounter.test.ts

import { renderHook, act } from '@testing-library/react';

import { describe, it, expect } from 'vitest';

import { useCounter } from './useCounter';


describe('useCounter', () => {

it('should initialize with default value', () => {

const { result } = renderHook(() => useCounter());

expect(result.current.count).toBe(0);

});


it('should initialize with custom value', () => {

const { result } = renderHook(() => useCounter(10));

expect(result.current.count).toBe(10);

});


it('should increment count', () => {

const { result } = renderHook(() => useCounter());


act(() => {

result.current.increment();

});


expect(result.current.count).toBe(1);

});


it('should decrement count', () => {

const { result } = renderHook(() => useCounter(5));


act(() => {

result.current.decrement();

});


expect(result.current.count).toBe(4);

});


it('should reset to initial value', () => {

const { result } = renderHook(() => useCounter(10));


act(() => {

result.current.increment();

result.current.increment();

});


expect(result.current.count).toBe(12);


act(() => {

result.current.reset();

});


expect(result.current.count).toBe(10);

});

});

`


Test Fixtures and Factories


`typescript

// tests/fixtures/user.fixture.ts

import { faker } from '@faker-js/faker';


export function createUserFixture(overrides?: Partial<User>): User {

return {

id: faker.string.uuid(),

name: faker.person.fullName(),

email: faker.internet.email(),

createdAt: faker.date.past(),

...overrides,

};

}


export function createUsersFixture(count: number): User[] {

return Array.from({ length: count }, () => createUserFixture());

}


// Usage in tests

import { createUserFixture, createUsersFixture } from '../fixtures/user.fixture';


describe('UserService', () => {

it('should process user', () => {

const user = createUserFixture({ name: 'John Doe' });

// Use user in test

});


it('should handle multiple users', () => {

const users = createUsersFixture(10);

// Use users in test

});

});

`


Snapshot Testing


`typescript

// components/UserCard.test.tsx

import { render } from '@testing-library/react';

import { describe, it, expect } from 'vitest';

import { UserCard } from './UserCard';


describe('UserCard', () => {

it('should match snapshot', () => {

const user = {

id: '1',

name: 'John Doe',

email: 'john@example.com',

avatar: 'https://example.com/avatar.jpg',

};


const { container } = render(<UserCard user={user} />);


expect(container.firstChild).toMatchSnapshot();

});


it('should match snapshot with loading state', () => {

const { container } = render(<UserCard loading />);

expect(container.firstChild).toMatchSnapshot();

});

});

`


Coverage Reports


`typescript

// package.json

{

"scripts": {

"test": "vitest",

"test:coverage": "vitest --coverage",

"test:ui": "vitest --ui"

}

}

`


Best Practices


1. Follow AAA Pattern: Arrange, Act, Assert

2. One assertion per test: Or logically related assertions

3. Descriptive test names: Should describe what is being tested

4. Use beforeEach/afterEach: For setup and teardown

5. Mock external dependencies: Keep tests isolated

6. Test edge cases: Not just happy paths

7. Avoid implementation details: Test behavior, not implementation

8. Use test factories: For consistent test data

9. Keep tests fast: Mock slow operations

10. Write tests first (TDD): When possible

11. Maintain test coverage: Aim for 80%+ coverage

12. Use TypeScript: For type-safe tests

13. Test error handling: Not just success cases

14. Use data-testid sparingly: Prefer semantic queries

15. Clean up after tests: Prevent test pollution


Common Patterns


Test Organization


`typescript

describe('UserService', () => {

describe('createUser', () => {

it('should create user successfully', () => {});

it('should throw error if email exists', () => {});

it('should hash password', () => {});

});


describe('updateUser', () => {

it('should update user', () => {});

it('should throw error if not found', () => {});

});

});

`


Testing Promises


`typescript

// Using async/await

it('should fetch user', async () => {

const user = await service.fetchUser('1');

expect(user).toBeDefined();

});


// Testing rejections

it('should throw error', async () => {

await expect(service.fetchUser('invalid')).rejects.toThrow('Not found');

});

`


Testing Timers


`typescript

import { vi } from 'vitest';


it('should call function after delay', () => {

vi.useFakeTimers();


const callback = vi.fn();

setTimeout(callback, 1000);


expect(callback).not.toHaveBeenCalled();


vi.advanceTimersByTime(1000);


expect(callback).toHaveBeenCalled();


vi.useRealTimers();

});

``


Resources


  • Jest Documentation: https://jestjs.io/
  • Vitest Documentation: https://vitest.dev/
  • Testing Library: https://testing-library.com/
  • Kent C. Dodds Testing Blog: https://kentcdodds.com/blog/

Informacion

Estado
Activo
Origen upstream
Usos 3
Ultimo uso 23/01 16:57
Actualizado 09/01/2026 02:14

Archivo origen

Z:\Repositorios\core\upstream\plugins\javascript-typescript\skills\javascript-testing-patterns\SKILL.md