Testing Guide
This guide explains how to write and run tests in the Plus261 project. Testing ensures code quality and prevents regressions.
What You’ll Learn
Section intitulée « What You’ll Learn »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
Overview
Section intitulée « Overview »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 |
Testing Setup
Section intitulée « Testing Setup »Running Tests
Section intitulée « Running Tests »# Run all testsbun test
# Run tests in watch modebun test:watch
# Run tests for a specific filebun test path/to/test/file.test.ts
# Run tests with coveragebun test --coverageTest File Structure
Section intitulée « Test File Structure »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 testsUnit Tests
Section intitulée « Unit Tests »Unit tests verify individual functions and classes in isolation.
Example: Service Test
Section intitulée « Example: Service Test »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(); });});Best Practices
Section intitulée « Best Practices »- Test One Thing - Each test should verify one behavior
- Use Descriptive Names - Test names should describe what they test
- Arrange-Act-Assert - Structure tests clearly
- Mock Dependencies - Isolate the code under test
- Test Edge Cases - Test error conditions and boundaries
Integration Tests
Section intitulée « Integration Tests »Integration tests verify that multiple components work together correctly.
Example: API Integration Test
Section intitulée « Example: API Integration Test »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(); });});Testing GraphQL
Section intitulée « Testing GraphQL »Testing Queries
Section intitulée « Testing Queries »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();});Testing Mutations
Section intitulée « Testing Mutations »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();});Testing Authorization
Section intitulée « Testing Authorization »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(); });});Testing Database Operations
Section intitulée « Testing Database Operations »Using Test Database
Section intitulée « Using Test Database »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); });});Mocking Services
Section intitulée « Mocking Services »import { vi } from 'vitest';
const mockUserService = { getUserById: vi.fn().mockResolvedValue({ id: 'user-123', email: 'test@example.com', }),};
// Use mock in testsit('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');});Test Coverage
Section intitulée « Test Coverage »Coverage Goals
Section intitulée « Coverage Goals »- Critical paths: 100% coverage
- Business logic: >80% coverage
- Utilities: >70% coverage
- Overall: >75% coverage
Viewing Coverage
Section intitulée « Viewing Coverage »# Generate coverage reportbun test --coverage
# View coverage reportopen coverage/index.htmlTesting Best Practices
Section intitulée « Testing Best Practices »1. Write Tests First (TDD)
Section intitulée « 1. Write Tests First (TDD) »Write tests before implementation when possible:
// 1. Write testit('should calculate total price', () => { const items = [ { price: 10, quantity: 2 }, { price: 5, quantity: 3 }, ]; expect(calculateTotal(items)).toBe(35);});
// 2. Implement functionfunction calculateTotal(items) { return items.reduce((sum, item) => sum + item.price * item.quantity, 0);}2. Test Behavior, Not Implementation
Section intitulée « 2. Test Behavior, Not Implementation »// ✅ Good - tests behaviorit('should return user by email', async () => { const user = await userService.findByEmail('test@example.com'); expect(user.email).toBe('test@example.com');});
// ❌ Bad - tests implementationit('should call database.query', async () => { await userService.findByEmail('test@example.com'); expect(db.query).toHaveBeenCalled();});3. Use Descriptive Test Names
Section intitulée « 3. Use Descriptive Test Names »// ✅ Goodit('should throw NotFoundError when user does not exist', async () => { // ...});
// ❌ Badit('test getUserById', async () => { // ...});4. Test Error Cases
Section intitulée « 4. Test Error Cases »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');});5. Keep Tests Independent
Section intitulée « 5. Keep Tests Independent »Each test should be able to run independently:
// ✅ Good - each test sets up its own databeforeEach(() => { // Set up test data});
// ❌ Bad - tests depend on each otherit('test 1', () => { // Creates data});
it('test 2', () => { // Depends on test 1's data});Common Testing Patterns
Section intitulée « Common Testing Patterns »Testing Async Functions
Section intitulée « Testing Async Functions »it('should handle async operations', async () => { const result = await asyncFunction(); expect(result).toBeDefined();});Testing Promises
Section intitulée « Testing Promises »it('should reject on error', async () => { await expect(failingFunction()).rejects.toThrow();});Debugging Tests
Section intitulée « Debugging Tests »Running Specific Tests
Section intitulée « Running Specific Tests »# Run tests matching a patternbun test --grep "UserService"
# Run only tests in a filebun test user.service.test.tsUsing Debugger
Section intitulée « Using Debugger »it('should debug test', () => { debugger; // Set breakpoint // Test code});Run with debugger:
bun --inspect test user.service.test.tsQuick Reference
Section intitulée « Quick Reference »| 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 |
Next Steps
Section intitulée « Next Steps »Continue learning:
- 🐛 Debugging Guide - Debugging techniques
- 🛠️ Development Workflow - Development process
- 📝 Code Style - Coding conventions
- 🧪 Contributing Testing - Testing standards
Related Topics
Section intitulée « Related Topics »- Development Workflow - Development process
- Debugging - Debugging techniques
- Code Style - Coding conventions
- Contributing Testing - Testing standards