Aller au contenu

Testing Guide

This guide explains how to write and run tests in the Plus261 project. Testing ensures code quality and prevents regressions.

This guide will help you understand:

  • ✅ How to set up and run tests
  • ✅ How to write unit, integration, and E2E tests
  • ✅ How to test GraphQL queries and mutations
  • ✅ How to test authorization and permissions
  • ✅ Best practices for testing
  • ✅ How to debug tests

Testing ensures code quality and prevents regressions. The project uses multiple testing strategies:

Strategy Purpose When to Use
Unit Tests Test individual functions and classes Test business logic, utilities
Integration Tests Test component interactions Test API endpoints, database operations
E2E Tests Test complete user flows Test critical user journeys
Type Checking TypeScript type safety Compile-time type checking
Fenêtre de terminal
# Run all tests
bun test
# Run tests in watch mode
bun test:watch
# Run tests for a specific file
bun test path/to/test/file.test.ts
# Run tests with coverage
bun test --coverage

Tests are located alongside source files or in __tests__ directories:

module-name/
├── module-name.service.ts
├── module-name.service.test.ts → Unit tests
└── __tests__/
└── integration.test.ts → Integration tests

Unit tests verify individual functions and classes in isolation.

user.service.test.ts
import { describe, it, expect, beforeEach } from 'bun:test';
import { UserService } from './user.service';
import { db } from '../database';
describe('UserService', () => {
let userService: UserService;
beforeEach(() => {
userService = new UserService(db);
});
it('should create a user', async () => {
const userData = {
email: 'test@example.com',
name: 'Test User',
};
const user = await userService.createUser(userData);
expect(user).toBeDefined();
expect(user.email).toBe(userData.email);
expect(user.name).toBe(userData.name);
});
it('should throw error for duplicate email', async () => {
const userData = {
email: 'existing@example.com',
name: 'Test User',
};
await expect(userService.createUser(userData)).rejects.toThrow();
});
});
  1. Test One Thing - Each test should verify one behavior
  2. Use Descriptive Names - Test names should describe what they test
  3. Arrange-Act-Assert - Structure tests clearly
  4. Mock Dependencies - Isolate the code under test
  5. Test Edge Cases - Test error conditions and boundaries

Integration tests verify that multiple components work together correctly.

api.integration.test.ts
import { describe, it, expect, beforeAll } from 'bun:test';
import { createYoga } from 'graphql-yoga';
import { schema } from '../create-schema';
describe('Product API', () => {
let yoga: ReturnType<typeof createYoga>;
beforeAll(() => {
yoga = createYoga({ schema });
});
it('should query products', async () => {
const query = `
query {
products {
id
name
}
}
`;
const response = await yoga.fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query }),
});
const result = await response.json();
expect(result.data.products).toBeDefined();
});
});
import { execute } from 'graphql';
import { schema } from '../create-schema';
it('should execute product query', async () => {
const query = `
query GetProducts {
products(filter: { status: { eq: "ACTIVE" } }) {
id
name
}
}
`;
const result = await execute({
schema,
document: parse(query),
});
expect(result.errors).toBeUndefined();
expect(result.data?.products).toBeDefined();
});
it('should create a product', async () => {
const mutation = `
mutation CreateProduct($input: ProductInput!) {
createProduct(input: $input) {
id
name
}
}
`;
const variables = {
input: {
name: 'Test Product',
status: 'ACTIVE',
},
};
const result = await execute({
schema,
document: parse(mutation),
variableValues: variables,
});
expect(result.errors).toBeUndefined();
expect(result.data?.createProduct).toBeDefined();
});

Test that authorization works correctly:

describe('Product Authorization', () => {
it('should deny access without permission', async () => {
const context = createContext({ userId: 'user-123' });
// Mock user without product.view permission
const result = await execute({
schema,
document: parse(query),
contextValue: context,
});
expect(result.errors).toBeDefined();
expect(result.errors[0].message).toContain('Unauthorized');
});
it('should allow access with permission', async () => {
const context = createContext({
userId: 'user-123',
permissions: ['product.view'],
});
const result = await execute({
schema,
document: parse(query),
contextValue: context,
});
expect(result.errors).toBeUndefined();
});
});
import { db } from '../database';
import { beforeEach, afterEach } from 'bun:test';
describe('Database Tests', () => {
beforeEach(async () => {
// Set up test data
await db.insert(users).values({
id: 'test-user-1',
email: 'test@example.com',
});
});
afterEach(async () => {
// Clean up test data
await db.delete(users);
});
it('should query users', async () => {
const result = await db.select().from(users);
expect(result.length).toBeGreaterThan(0);
});
});
import { vi } from 'vitest';
const mockUserService = {
getUserById: vi.fn().mockResolvedValue({
id: 'user-123',
email: 'test@example.com',
}),
};
// Use mock in tests
it('should use mocked service', async () => {
const user = await mockUserService.getUserById('user-123');
expect(mockUserService.getUserById).toHaveBeenCalledWith('user-123');
expect(user.email).toBe('test@example.com');
});
  • Critical paths: 100% coverage
  • Business logic: >80% coverage
  • Utilities: >70% coverage
  • Overall: >75% coverage
Fenêtre de terminal
# Generate coverage report
bun test --coverage
# View coverage report
open coverage/index.html

Write tests before implementation when possible:

// 1. Write test
it('should calculate total price', () => {
const items = [
{ price: 10, quantity: 2 },
{ price: 5, quantity: 3 },
];
expect(calculateTotal(items)).toBe(35);
});
// 2. Implement function
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
// ✅ Good - tests behavior
it('should return user by email', async () => {
const user = await userService.findByEmail('test@example.com');
expect(user.email).toBe('test@example.com');
});
// ❌ Bad - tests implementation
it('should call database.query', async () => {
await userService.findByEmail('test@example.com');
expect(db.query).toHaveBeenCalled();
});
// ✅ Good
it('should throw NotFoundError when user does not exist', async () => {
// ...
});
// ❌ Bad
it('test getUserById', async () => {
// ...
});
it('should handle database errors', async () => {
// Mock database error
mockDb.query.users.findMany.mockRejectedValue(new Error('DB Error'));
await expect(userService.getUsers()).rejects.toThrow('DB Error');
});

Each test should be able to run independently:

// ✅ Good - each test sets up its own data
beforeEach(() => {
// Set up test data
});
// ❌ Bad - tests depend on each other
it('test 1', () => {
// Creates data
});
it('test 2', () => {
// Depends on test 1's data
});
it('should handle async operations', async () => {
const result = await asyncFunction();
expect(result).toBeDefined();
});
it('should reject on error', async () => {
await expect(failingFunction()).rejects.toThrow();
});
Fenêtre de terminal
# Run tests matching a pattern
bun test --grep "UserService"
# Run only tests in a file
bun test user.service.test.ts
it('should debug test', () => {
debugger; // Set breakpoint
// Test code
});

Run with debugger:

Fenêtre de terminal
bun --inspect test user.service.test.ts
Task Command
Run all tests bun test
Watch mode bun test:watch
Coverage bun test --coverage
Specific file bun test path/to/file.test.ts

Continue learning:

  1. 🐛 Debugging Guide - Debugging techniques
  2. 🛠️ Development Workflow - Development process
  3. 📝 Code Style - Coding conventions
  4. 🧪 Contributing Testing - Testing standards