🎯

javascript-testing-patterns

🎯Skill

from ovachiever/droid-tings

VibeIndex|
What it does

Implements comprehensive JavaScript/TypeScript testing strategies using Jest, Vitest, and Testing Library for robust unit, integration, and end-to-end testing workflows.

πŸ“¦

Part of

ovachiever/droid-tings(370 items)

javascript-testing-patterns

Installation

git cloneClone repository
git clone https://github.com/ovachiever/droid-tings.git
πŸ“– Extracted from docs: ovachiever/droid-tings
14Installs
-
AddedFeb 4, 2026

Skill Details

SKILL.md

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.

Overview

# 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: ['/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: ['/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 = 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 {

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 {

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 {

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',

'

Test Body

'

);

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;

create(user: User): Promise;

}

export class UserService {

constructor(private userRepository: IUserRepository) {}

async getUser(id: string): Promise {

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

if (!user) {

throw new Error('User not found');

}

return user;

}

async createUser(userData: CreateUserDTO): Promise {

// 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 {

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 (

type="text"

placeholder="Name"

value={name}

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

data-testid="name-input"

/>

type="email"

placeholder="Email"

value={email}

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

data-testid="email-input"

/>

);

}

// 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();

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

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

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

});

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

render();

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();

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 {

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();

expect(container.firstChild).toMatchSnapshot();

});

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

const { container } = render();

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/