| name | javascript-testing-patterns |
| description | 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. |
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:
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:
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
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;
}
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((-, -)).(-);
});
(, {
((, )).();
((, )).();
});
});
(, {
(, {
((, )).();
});
(, {
((, )).();
});
(, {
( (, )).();
});
});
});
Pattern 2: Testing Classes
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();
}
updated = { ...user, ...updates };
..(id, updated);
updated;
}
(: ): {
..(id);
}
}
{ describe, it, expect, beforeEach } ;
{ } ;
(, {
: ;
( {
service = ();
});
(, {
(, {
user = { : , : , : };
created = service.(user);
(created).(user);
(service.()).(user);
});
(, {
user = { : , : , : };
service.(user);
( service.(user)).();
});
});
(, {
(, {
user = { : , : , : };
service.(user);
updated = service.(, { : });
(updated.).();
(updated.).();
});
(, {
( service.(, { : })).(
,
);
});
});
});
Pattern 3: Testing Async Functions
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();
}
}
import { describe, it, expect, vi, beforeEach } from "vitest";
import { ApiService } from ;
. = vi.();
(, {
: ;
( {
service = ();
vi.();
});
(, {
(, () => {
mockUser = { : , : , : };
(fetch ).({
: ,
: () => mockUser,
});
user = service.();
(user).(mockUser);
(fetch).();
});
(, () => {
(fetch ).({
: ,
});
(service.())..();
});
});
(, {
(, () => {
newUser = { : , : };
createdUser = { : , ...newUser };
(fetch ).({
: ,
: () => createdUser,
});
user = service.(newUser);
(user).(createdUser);
(fetch).(
,
expect.({
: ,
: .(newUser),
}),
);
});
});
});
Mocking Patterns
Pattern 1: Mocking Modules
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,
});
}
}
import { describe, it, expect, vi, beforeEach } from "vitest";
import { EmailService } from "./email.service";
vi.mock("nodemailer", () => ({
default: {
createTransport: vi.fn( ({
: vi.().({ : }),
})),
},
}));
(, {
: ;
( {
service = ();
});
(, () => {
service.(
,
,
,
);
(service[].).(
expect.({
: ,
: ,
}),
);
});
});
Pattern 2: Dependency Injection for Testing
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> {
const user = { id: generateId(), ...userData };
return this..(user);
}
}
{ describe, it, expect, vi, beforeEach } ;
{ , } ;
(, {
: ;
: ;
( {
mockRepository = {
: vi.(),
: vi.(),
};
service = (mockRepository);
});
(, {
(, () => {
mockUser = { : , : , : };
vi.(mockRepository.).(mockUser);
user = service.();
(user).(mockUser);
(mockRepository.).();
});
(, () => {
vi.(mockRepository.).();
(service.())..();
});
});
(, {
(, () => {
userData = { : , : };
createdUser = { : , ...userData };
vi.(mockRepository.).(createdUser);
user = service.(userData);
(user).(createdUser);
(mockRepository.).();
});
});
});
Pattern 3: Spying on Functions
export const logger = {
info: (message: string) => console.log(`INFO: ${message}`),
error: (message: string) => console.error(`ERROR: ${message}`),
};
import { logger } from "../utils/logger";
export class OrderService {
async processOrder(orderId: string): Promise<void> {
logger.info(`Processing order ${orderId}`);
logger.info(`Order ${orderId} processed successfully`);
}
}
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { OrderService } from "./order.service";
import { logger } from "../utils/logger";
(, {
: ;
: ;
( {
service = ();
loggerSpy = vi.(logger, );
});
( {
loggerSpy.();
});
(, () => {
service.();
(loggerSpy).();
(loggerSpy).();
(loggerSpy).();
});
});
Integration Testing
Pattern 1: API Integration Tests
import request from "supertest";
import { app } from "../../src/app";
import { pool } from "../../src/config/database";
describe("User API Integration Tests", () => {
beforeAll(async () => {
await pool.query("CREATE TABLE IF NOT EXISTS users (...)");
});
afterAll(async () => {
await pool.query("DROP TABLE IF EXISTS users");
await pool.end();
});
beforeEach(async () => {
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 = (app)
.()
.(userData)
.();
(response.).({
: userData.,
: userData.,
});
(response.).();
(response.)..();
});
(, () => {
userData = {
: ,
: ,
: ,
};
response = (app)
.()
.(userData)
.();
(response.).();
});
(, () => {
userData = {
: ,
: ,
: ,
};
(app).().(userData);
response = (app)
.()
.(userData)
.();
(response..).();
});
});
(, {
(, () => {
createResponse = (app).().({
: ,
: ,
: ,
});
userId = createResponse..;
response = (app)
.()
.();
(response.).({
: userId,
: ,
: ,
});
});
(, () => {
(app).().();
});
});
(, {
(, () => {
(app).().();
});
(, () => {
(app).().({
: ,
: ,
: ,
});
loginResponse = (app).().({
: ,
: ,
});
token = loginResponse..;
response = (app)
.()
.(, )
.();
(response..).();
});
});
});
Pattern 2: Database Integration Tests
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);
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");
pool.();
});
( () => {
pool.();
});
(, () => {
user = repository.({
: ,
: ,
: ,
});
(user).();
(user.).();
(user.).();
});
(, () => {
repository.({
: ,
: ,
: ,
});
user = repository.();
(user).();
(user?.).();
});
(, () => {
user = repository.();
(user).();
});
});
Frontend Testing with Testing Library
Pattern 1: React Component Testing
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"
=
= => setEmail(e.target.value)}
data-testid="email-input"
/>
Submit
);
}
{ render, screen, fireEvent } ;
{ describe, it, expect, vi } ;
{ } ;
(, {
(, {
();
(screen.()).();
(screen.()).();
(screen.(, { : })).();
});
(, {
();
nameInput = screen.() ;
emailInput = screen.() ;
fireEvent.(nameInput, { : { : } });
fireEvent.(emailInput, { : { : } });
(nameInput.).();
(emailInput.).();
});
(, {
onSubmit = vi.();
();
fireEvent.(screen.(), {
: { : },
});
fireEvent.(screen.(), {
: { : },
});
fireEvent.(screen.(, { : }));
(onSubmit).({
: ,
: ,
});
});
});
Pattern 2: Testing Hooks
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 };
}
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(() => ());
(result..).();
});
(, {
{ result } = ( ());
(result..).();
});
(, {
{ result } = ( ());
( {
result..();
});
(result..).();
});
(, {
{ result } = ( ());
( {
result..();
});
(result..).();
});
(, {
{ result } = ( ());
( {
result..();
result..();
});
(result..).();
( {
result..();
});
(result..).();
});
});
Test Fixtures and Factories
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());
}
import {
createUserFixture,
createUsersFixture,
} from "../fixtures/user.fixture";
describe("UserService", () => {
it("should process user", () => {
const user = createUserFixture({ name: });
});
(, {
users = ();
});
});
Snapshot Testing
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
{
"scripts": {
"test": "vitest",
"test:coverage": "vitest --coverage",
"test:ui": "vitest --ui"
}
}
Best Practices
- Follow AAA Pattern: Arrange, Act, Assert
- One assertion per test: Or logically related assertions
- Descriptive test names: Should describe what is being tested
- Use beforeEach/afterEach: For setup and teardown
- Mock external dependencies: Keep tests isolated
- Test edge cases: Not just happy paths
- Avoid implementation details: Test behavior, not implementation
- Use test factories: For consistent test data
- Keep tests fast: Mock slow operations
- Write tests first (TDD): When possible
- Maintain test coverage: Aim for 80%+ coverage
- Use TypeScript: For type-safe tests
- Test error handling: Not just success cases
- Use data-testid sparingly: Prefer semantic queries
- Clean up after tests: Prevent test pollution
Common Patterns
Test Organization
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
it("should fetch user", async () => {
const user = await service.fetchUser("1");
expect(user).toBeDefined();
});
it("should throw error", async () => {
await expect(service.fetchUser("invalid")).rejects.toThrow("Not found");
});
Testing Timers
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