--- description: How to test backend APIs and services --- # Testing Workflow Follow these steps to write and run tests for the E-Learning Platform backend using TypeScript and Jest. --- ## Test Structure ``` tests/ ├── unit/ # Unit tests (services, utilities) ├── integration/ # Integration tests (API endpoints) ├── fixtures/ # Test data └── setup.js # Test setup ``` --- ## Step 1: Setup Test Environment Create `tests/setup.ts`: ```typescript import { PrismaClient } from '@prisma/client'; import bcrypt from 'bcrypt'; const prisma = new PrismaClient(); beforeAll(async () => { // Setup test database await prisma.$executeRaw`TRUNCATE TABLE "User" CASCADE`; // Create test data await createTestUsers(); await createTestCourses(); }); afterAll(async () => { // Cleanup await prisma.$disconnect(); }); async function createTestUsers() { // Create admin, instructor, student await prisma.user.createMany({ data: [ { username: 'admin', email: 'admin@test.com', password: await bcrypt.hash('password123', 10), role_id: 1 }, { username: 'instructor1', email: 'instructor@test.com', password: await bcrypt.hash('password123', 10), role_id: 2 }, { username: 'student1', email: 'student@test.com', password: await bcrypt.hash('password123', 10), role_id: 3 } ] }); } ``` --- ## Step 2: Write Unit Tests Create `tests/unit/course.service.test.ts`: ```typescript import { courseService } from '../../src/services/course.service'; import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); describe('Course Service', () => { describe('createCourse', () => { it('should create course with valid data', async () => { const courseData = { title: { th: 'ทดสอบ', en: 'Test' }, description: { th: 'รายละเอียด', en: 'Description' }, category_id: 1, price: 990, is_free: false }; const course = await courseService.create(courseData, 1); expect(course).toHaveProperty('id'); expect(course.status).toBe('DRAFT'); expect(course.title.th).toBe('ทดสอบ'); }); it('should throw error with invalid category', async () => { const courseData = { title: { th: 'ทดสอบ', en: 'Test' }, category_id: 9999, // Invalid price: 990 }; await expect( courseService.create(courseData, 1) ).rejects.toThrow(); }); }); describe('checkLessonAccess', () => { it('should allow access to unlocked lesson', async () => { const result = await courseService.checkLessonAccess(1, 1); expect(result.allowed).toBe(true); }); it('should deny access to locked lesson', async () => { const result = await courseService.checkLessonAccess(1, 5); expect(result.allowed).toBe(false); expect(result.reason).toBe('incomplete_prerequisites'); }); }); }); ``` --- ## Step 3: Write Integration Tests Create `tests/integration/courses.test.ts`: ```typescript import request from 'supertest'; import app from '../../src/app'; describe('Course API', () => { let adminToken: string; let instructorToken: string; let studentToken: string; beforeAll(async () => { // Login as different users const adminRes = await request(app) .post('/api/auth/login') .send({ username: 'admin', password: 'password123' }); adminToken = adminRes.body.token; const instructorRes = await request(app) .post('/api/auth/login') .send({ username: 'instructor1', password: 'password123' }); instructorToken = instructorRes.body.token; const studentRes = await request(app) .post('/api/auth/login') .send({ username: 'student1', password: 'password123' }); studentToken = studentRes.body.token; }); describe('POST /api/instructor/courses', () => { it('should create course as instructor', async () => { const courseData = { title: { th: 'คอร์สใหม่', en: 'New Course' }, description: { th: 'รายละเอียด', en: 'Description' }, category_id: 1, price: 990, is_free: false }; const response = await request(app) .post('/api/instructor/courses') .set('Authorization', `Bearer ${instructorToken}`) .send(courseData); expect(response.status).toBe(201); expect(response.body).toHaveProperty('id'); expect(response.body.status).toBe('DRAFT'); }); it('should return 403 as student', async () => { const response = await request(app) .post('/api/instructor/courses') .set('Authorization', `Bearer ${studentToken}`) .send({}); expect(response.status).toBe(403); }); it('should return 401 without auth', async () => { const response = await request(app) .post('/api/instructor/courses') .send({}); expect(response.status).toBe(401); }); }); describe('GET /api/courses', () => { it('should list public courses', async () => { const response = await request(app) .get('/api/courses') .query({ page: 1, limit: 10 }); expect(response.status).toBe(200); expect(response.body).toHaveProperty('data'); expect(response.body).toHaveProperty('pagination'); expect(Array.isArray(response.body.data)).toBe(true); }); it('should filter by category', async () => { const response = await request(app) .get('/api/courses') .query({ category: 1 }); expect(response.status).toBe(200); response.body.data.forEach((course: any) => { expect(course.category_id).toBe(1); }); }); }); }); ``` --- ## Step 4: Test File Uploads Create `tests/integration/file-upload.test.ts`: ```typescript import request from 'supertest'; import app from '../../src/app'; import path from 'path'; describe('File Upload API', () => { let instructorToken: string; beforeAll(async () => { const res = await request(app) .post('/api/auth/login') .send({ username: 'instructor1', password: 'password123' }); instructorToken = res.body.token; }); describe('POST /api/instructor/lessons/:lessonId/attachments', () => { it('should upload PDF attachment', async () => { const filePath = path.join(__dirname, '../fixtures/test.pdf'); const response = await request(app) .post('/api/instructor/courses/1/lessons/1/attachments') .set('Authorization', `Bearer ${instructorToken}`) .attach('file', filePath) .field('description_th', 'ไฟล์ทดสอบ') .field('description_en', 'Test file'); expect(response.status).toBe(201); expect(response.body).toHaveProperty('file_name'); expect(response.body.mime_type).toBe('application/pdf'); }); it('should reject invalid file type', async () => { const filePath = path.join(__dirname, '../fixtures/test.exe'); const response = await request(app) .post('/api/instructor/courses/1/lessons/1/attachments') .set('Authorization', `Bearer ${instructorToken}`) .attach('file', filePath); expect(response.status).toBe(422); expect(response.body.error.code).toBe('INVALID_FILE_TYPE'); }); it('should reject file too large', async () => { // Mock large file const response = await request(app) .post('/api/instructor/courses/1/lessons/1/attachments') .set('Authorization', `Bearer ${instructorToken}`) .attach('file', Buffer.alloc(101 * 1024 * 1024)); // 101 MB expect(response.status).toBe(422); expect(response.body.error.code).toBe('FILE_TOO_LARGE'); }); }); }); ``` --- ## Step 5: Run Tests // turbo Run all tests: ```bash npm test ``` // turbo Run specific test file: ```bash npm test -- courses.test.js ``` // turbo Run with coverage: ```bash npm test -- --coverage ``` // turbo Run in watch mode: ```bash npm test -- --watch ``` --- ## Test Configuration Update `package.json`: ```json { "scripts": { "test": "jest --runInBand", "test:watch": "jest --watch", "test:coverage": "jest --coverage" }, "jest": { "preset": "ts-jest", "testEnvironment": "node", "coveragePathIgnorePatterns": ["/node_modules/"], "setupFilesAfterEnv": ["/tests/setup.ts"], "testMatch": ["**/tests/**/*.test.ts"], "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"] } } ``` --- ## Mocking ### Mock Prisma ```javascript jest.mock('@prisma/client', () => { const mockPrisma = { user: { findUnique: jest.fn(), create: jest.fn() }, course: { findMany: jest.fn(), create: jest.fn() } }; return { PrismaClient: jest.fn(() => mockPrisma) }; }); ``` ### Mock S3/MinIO ```javascript jest.mock('../src/services/s3.service', () => ({ uploadFile: jest.fn().mockResolvedValue({ url: 'https://s3.example.com/test.pdf', key: 'test.pdf' }), deleteFile: jest.fn().mockResolvedValue(true) })); ``` --- ## Test Fixtures Create `tests/fixtures/courses.json`: ```json { "validCourse": { "title": { "th": "คอร์สทดสอบ", "en": "Test Course" }, "description": { "th": "รายละเอียด", "en": "Description" }, "category_id": 1, "price": 990, "is_free": false }, "invalidCourse": { "title": "Invalid" } } ``` Use in tests: ```javascript const fixtures = require('../fixtures/courses.json'); it('should create course', async () => { const response = await request(app) .post('/api/instructor/courses') .send(fixtures.validCourse); expect(response.status).toBe(201); }); ``` --- ## Checklist - [ ] Test setup configured - [ ] Unit tests written for services - [ ] Integration tests written for API endpoints - [ ] File upload tests included - [ ] Authentication/authorization tested - [ ] Error cases tested - [ ] All tests passing - [ ] Coverage > 80% --- ## Best Practices 1. **Test Isolation**: Each test should be independent 2. **Descriptive Names**: Use clear test descriptions 3. **AAA Pattern**: Arrange, Act, Assert 4. **Mock External Services**: Don't call real APIs in tests 5. **Test Edge Cases**: Not just happy path 6. **Clean Up**: Reset database state between tests 7. **Fast Tests**: Keep tests fast (< 5 seconds total)