Testing Guide
Overview
This guide explains the testing infrastructure, how to write tests, and best practices for maintaining code quality.
Testing Stack
- Jest: Unit and integration test runner
- React Testing Library: Component testing
- MSW (Mock Service Worker): HTTP request mocking
- @testing-library/jest-dom: DOM matchers
Running Tests
Run all tests
npm test
Run tests in watch mode
npm run test:watch
Generate coverage report
npm run test:coverage
Run specific test file
npm test -- cart-slice.test
Run tests matching pattern
npm test -- --testNamePattern="addItemToCart"
Test Structure
Tests are organized in __tests__ directories parallel to source files:
src/
├── redux/
│ └── features/
│ ├── __tests__/
│ │ └── cart-slice.test.ts
│ └── cart-slice.ts
├── lib/
│ ├── __tests__/
│ │ └── validation.test.ts
│ └── validation-schemas.ts
└── components/
└── features/
└── shop/
└── ProductItem/
├── __tests__/
│ └── ProductItem.test.tsx
└── index.tsx
Writing Tests
Redux/Reducer Tests
Test Redux slices using direct reducer calls:
import { cart, addItemToCart, removeItemFromCart } from "@/redux/features/cartSlice";
describe("Cart Slice", () => {
const initialState = { items: [] };
it("should add item to cart", () => {
const item = { id: 1, title: "Product", price: 100, quantity: 1 };
const state = cart.reducer(initialState, addItemToCart(item));
expect(state.items).toHaveLength(1);
expect(state.items[0]).toEqual(item);
});
});
Key Points:
- Use
reducer()function directly for unit testing - Test state mutations and pure functions
- Cover edge cases and error scenarios
Validation/Schema Tests
Test Zod schemas using safeParse():
import { signupSchema } from "@/lib/validation-schemas";
describe("signupSchema", () => {
it("should validate correct data", () => {
const result = signupSchema.safeParse({
email: "test@example.com",
password: "password123",
passwordConfirm: "password123",
});
expect(result.success).toBe(true);
});
it("should reject invalid email", () => {
const result = signupSchema.safeParse({
email: "invalid",
password: "password123",
passwordConfirm: "password123",
});
expect(result.success).toBe(false);
});
});
Key Points:
- Test both success and failure cases
- Validate error messages
- Test edge cases (empty strings, negative numbers, etc.)
Component Tests
Test components using React Testing Library:
import { render, screen } from "@testing-library/react";
import ProductItem from "@/components/features/shop/ProductItem";
describe("ProductItem", () => {
const mockProduct = {
id: 1,
title: "Test Product",
price: 100,
discountedPrice: 80,
};
it("should render product title", () => {
render(<ProductItem item={mockProduct} />);
expect(screen.getByText("Test Product")).toBeInTheDocument();
});
it("should display prices", () => {
render(<ProductItem item={mockProduct} />);
expect(screen.getByText("$80")).toBeInTheDocument();
});
});
Key Points:
- Test user-visible behavior, not implementation
- Use
screenqueries instead of container - Mock Next.js Image component
- Wrap components with necessary providers (Redux, SessionProvider)
API Route Tests
Test API endpoints directly:
import { GET } from "@/app/api/products/route";
import { NextRequest } from "next/server";
describe("GET /api/products", () => {
it("should return products list", async () => {
const request = new NextRequest(
new URL("http://localhost:3000/api/products")
);
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(Array.isArray(data)).toBe(true);
});
});
Key Points:
- Test with NextRequest directly
- Verify response status and format
- Test error cases and validation
- Mock database calls as needed
Mocking
Mocking Modules
jest.mock("@/lib/api/products", () => ({
getProducts: jest.fn(),
}));
Mocking Next.js Features
Already configured in jest.setup.ts:
next/routerandnext/navigationnext-auth/reactsessionnext/imagecomponent
MSW (Mock Service Worker)
For integration tests with HTTP requests:
// jest.setup.ts configures MSW
// Define handlers in src/mocks/handlers.ts
// Use in component tests that make API calls
Coverage Goals
- Overall: 60%+ coverage
- Critical features (auth, cart, checkout): 90%+
- Utilities: 85%+
- Components: 70%+
Coverage Report
Generate coverage report:
npm run test:coverage
View detailed report:
open coverage/lcov-report/index.html # macOS
xdg-open coverage/lcov-report/index.html # Linux
Best Practices
✅ DO
- Test behavior, not implementation
- Use descriptive test names
- Test happy path and edge cases
- Keep tests focused and isolated
- Use factories for test data
- Test error handling
- Clean up after tests (clear mocks)
❌ DON'T
- Test implementation details
- Create overly specific assertions
- Share state between tests
- Mock too much (test integration instead)
- Write tests that are brittle/flaky
- Ignore async/await issues
- Leave console.log in tests
Test Examples
Example 1: Reducer Test
describe("removeAllItemsFromCart", () => {
it("should clear cart with multiple items", () => {
const item1 = { id: 1, title: "P1", price: 100, discountedPrice: 80, quantity: 1 };
const item2 = { id: 2, title: "P2", price: 200, discountedPrice: 150, quantity: 1 };
let state = cart.reducer(initialState, addItemToCart(item1));
state = cart.reducer(state, addItemToCart(item2));
expect(state.items).toHaveLength(2);
state = cart.reducer(state, removeAllItemsFromCart());
expect(state.items).toHaveLength(0);
});
});
Example 2: Validation Test
describe("signupSchema", () => {
it("should reject non-matching passwords", () => {
const result = signupSchema.safeParse({
email: "test@example.com",
password: "password123",
passwordConfirm: "password456",
});
expect(result.success).toBe(false);
});
});
Example 3: Component Test
describe("ProductItem", () => {
it("should display discount percentage", () => {
const product = {
id: 1,
title: "Product",
price: 100,
discountedPrice: 80,
};
render(<ProductItem item={product} />);
// Calculate: (1 - 80/100) * 100 = 20%
expect(screen.getByText("20%")).toBeInTheDocument();
});
});
Debugging Tests
Run single test
npm test -- cart-slice.test.ts
Run in debug mode
node --inspect-brk node_modules/.bin/jest --runInBand
Then open chrome://inspect in Chrome
Print debug information
import { render, screen } from "@testing-library/react";
it("should render element", () => {
const { debug } = render(<Component />);
debug(); // Prints rendered HTML
});
Continuous Integration
GitHub Actions automatically runs tests on:
- Push to
mainordevelopbranches - Pull requests to
mainordevelop
Workflows defined in .github/workflows/:
test.yml: Run tests and buildlint.yml: Run linting and type checking
Tests must pass before merging to main.
Performance Testing
Monitor test execution time:
npm test -- --verbose
Common Issues
Test timeout
Increase Jest timeout:
jest.setTimeout(10000);
it("slow test", async () => {
// ...
}, 10000);
Module not found
Check path aliases in jest.config.ts match tsconfig.json:
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
}
Async/await issues
Use waitFor for async operations:
import { waitFor } from "@testing-library/react";
it("should load data", async () => {
render(<Component />);
await waitFor(() => {
expect(screen.getByText("Data loaded")).toBeInTheDocument();
});
});
Resources
Next Steps
- Write tests for critical features
- Aim for 60%+ overall coverage
- Monitor coverage in CI/CD
- Review and refactor tests regularly
- Keep tests maintainable and focused