Express.js Testing และ API Documentation
Express Developer•17 ธันวาคม 2567•Testing
ExpressTestingJestSwaggerAPI Documentation
การทดสอบและ documentation เป็นส่วนสำคัญในการพัฒนา Express.js APIs ที่มีคุณภาพและใช้งานง่าย
การติดตั้ง Testing Dependencies
npm install -D jest supertest npm install -D @types/jest @types/supertest # สำหรับ TypeScript
Jest Configuration
// package.json { "scripts": { "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage" }, "jest": { "testEnvironment": "node", "collectCoverageFrom": [ "src/**/*.js", "!src/server.js", "!src/config/**" ], "coverageDirectory": "coverage", "coverageReporters": ["text", "lcov", "html"] } }
Test Setup
// tests/setup.js const mongoose = require('mongoose') const { MongoMemoryServer } = require('mongodb-memory-server') let mongoServer // เริ่มต้น in-memory MongoDB สำหรับ testing beforeAll(async () => { mongoServer = await MongoMemoryServer.create() const mongoUri = mongoServer.getUri() await mongoose.connect(mongoUri, { useNewUrlParser: true, useUnifiedTopology: true }) }) // ล้าง database หลังแต่ละ test afterEach(async () => { const collections = mongoose.connection.collections for (const key in collections) { await collections[key].deleteMany({}) } }) // ปิด connection หลัง tests เสร็จ afterAll(async () => { await mongoose.connection.dropDatabase() await mongoose.connection.close() await mongoServer.stop() })
Unit Tests สำหรับ Models
// tests/models/User.test.js const User = require('../../models/User') require('../setup') describe('User Model', () => { describe('Validation', () => { test('should create a valid user', async () => { const userData = { name: 'John Doe', email: 'john@example.com', password: 'password123' } const user = new User(userData) const savedUser = await user.save() expect(savedUser._id).toBeDefined() expect(savedUser.name).toBe(userData.name) expect(savedUser.email).toBe(userData.email) expect(savedUser.role).toBe('user') // default value expect(savedUser.password).not.toBe(userData.password) // should be hashed }) test('should require name, email, and password', async () => { const user = new User({}) let error try { await user.save() } catch (err) { error = err } expect(error).toBeDefined() expect(error.errors.name).toBeDefined() expect(error.errors.email).toBeDefined() expect(error.errors.password).toBeDefined() }) test('should not accept invalid email format', async () => { const userData = { name: 'John Doe', email: 'invalid-email', password: 'password123' } const user = new User(userData) let error try { await user.save() } catch (err) { error = err } expect(error).toBeDefined() expect(error.errors.email).toBeDefined() }) }) describe('Methods', () => { test('should compare password correctly', async () => { const password = 'password123' const user = new User({ name: 'John Doe', email: 'john@example.com', password }) await user.save() const isMatch = await user.comparePassword(password) const isNotMatch = await user.comparePassword('wrongpassword') expect(isMatch).toBe(true) expect(isNotMatch).toBe(false) }) }) describe('Statics', () => { test('should find active users only', async () => { await User.create([ { name: 'Active User', email: 'active@example.com', password: 'pass123', isActive: true }, { name: 'Inactive User', email: 'inactive@example.com', password: 'pass123', isActive: false } ]) const activeUsers = await User.findActiveUsers() expect(activeUsers).toHaveLength(1) expect(activeUsers[0].name).toBe('Active User') }) }) })
Integration Tests สำหรับ API Routes
// tests/routes/auth.test.js const request = require('supertest') const app = require('../../app') const User = require('../../models/User') require('../setup') describe('Auth Routes', () => { describe('POST /api/auth/register', () => { test('should register a new user', async () => { const userData = { name: 'John Doe', email: 'john@example.com', password: 'password123' } const response = await request(app) .post('/api/auth/register') .send(userData) .expect(201) expect(response.body.success).toBe(true) expect(response.body.token).toBeDefined() expect(response.body.data.email).toBe(userData.email) // ตรวจสอบว่า user ถูกสร้างใน database const user = await User.findOne({ email: userData.email }) expect(user).toBeTruthy() }) test('should not register user with existing email', async () => { const userData = { name: 'John Doe', email: 'john@example.com', password: 'password123' } // สร้าง user ก่อน await User.create(userData) const response = await request(app) .post('/api/auth/register') .send(userData) .expect(400) expect(response.body.success).toBe(false) expect(response.body.message).toContain('already exists') }) test('should validate required fields', async () => { const response = await request(app) .post('/api/auth/register') .send({}) .expect(400) expect(response.body.success).toBe(false) expect(response.body.message).toBeDefined() }) }) describe('POST /api/auth/login', () => { beforeEach(async () => { const user = new User({ name: 'John Doe', email: 'john@example.com', password: 'password123' }) await user.save() }) test('should login with valid credentials', async () => { const response = await request(app) .post('/api/auth/login') .send({ email: 'john@example.com', password: 'password123' }) .expect(200) expect(response.body.success).toBe(true) expect(response.body.token).toBeDefined() expect(response.body.data.email).toBe('john@example.com') }) test('should not login with invalid credentials', async () => { const response = await request(app) .post('/api/auth/login') .send({ email: 'john@example.com', password: 'wrongpassword' }) .expect(401) expect(response.body.success).toBe(false) expect(response.body.message).toContain('Invalid credentials') }) }) })
Testing Middleware
// tests/middleware/auth.test.js const request = require('supertest') const jwt = require('jsonwebtoken') const app = require('../../app') const User = require('../../models/User') require('../setup') describe('Auth Middleware', () => { let user, token beforeEach(async () => { user = await User.create({ name: 'John Doe', email: 'john@example.com', password: 'password123' }) token = jwt.sign({ id: user._id }, process.env.JWT_SECRET) }) test('should allow access with valid token', async () => { const response = await request(app) .get('/api/auth/profile') .set('Authorization', `Bearer ${token}`) .expect(200) expect(response.body.success).toBe(true) expect(response.body.data.email).toBe(user.email) }) test('should deny access without token', async () => { const response = await request(app) .get('/api/auth/profile') .expect(401) expect(response.body.success).toBe(false) expect(response.body.message).toContain('token required') }) test('should deny access with invalid token', async () => { const response = await request(app) .get('/api/auth/profile') .set('Authorization', 'Bearer invalid-token') .expect(401) expect(response.body.success).toBe(false) expect(response.body.message).toContain('Invalid') }) })
API Documentation ด้วย Swagger
npm install swagger-jsdoc swagger-ui-express
// config/swagger.js const swaggerJsdoc = require('swagger-jsdoc') const swaggerUi = require('swagger-ui-express') const options = { definition: { openapi: '3.0.0', info: { title: 'Express Blog API', version: '1.0.0', description: 'A simple Express blog API with authentication', contact: { name: 'API Support', email: 'support@example.com' } }, servers: [ { url: 'http://localhost:3000', description: 'Development server' }, { url: 'https://api.example.com', description: 'Production server' } ], components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' } }, schemas: { User: { type: 'object', required: ['name', 'email', 'password'], properties: { id: { type: 'string', description: 'User ID' }, name: { type: 'string', description: 'User name' }, email: { type: 'string', format: 'email', description: 'User email' }, role: { type: 'string', enum: ['user', 'admin'], description: 'User role' } } }, Post: { type: 'object', required: ['title', 'content'], properties: { id: { type: 'string', description: 'Post ID' }, title: { type: 'string', description: 'Post title' }, content: { type: 'string', description: 'Post content' }, author: { $ref: '#/components/schemas/User' } } }, Error: { type: 'object', properties: { success: { type: 'boolean', example: false }, message: { type: 'string', description: 'Error message' } } } } } }, apis: ['./routes/*.js'] // paths to files containing OpenAPI definitions } const specs = swaggerJsdoc(options) module.exports = { specs, swaggerUi }
Swagger Annotations ใน Routes
// routes/auth.js const express = require('express') const router = express.Router() /** * @swagger * /api/auth/register: * post: * summary: Register a new user * tags: [Authentication] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - name * - email * - password * properties: * name: * type: string * example: John Doe * email: * type: string * format: email * example: john@example.com * password: * type: string * format: password * example: password123 * responses: * 201: * description: User registered successfully * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * example: true * message: * type: string * example: User registered successfully * token: * type: string * example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... * data: * $ref: '#/components/schemas/User' * 400: * description: Validation error * content: * application/json: * schema: * $ref: '#/components/schemas/Error' */ router.post('/register', registerUser) /** * @swagger * /api/auth/login: * post: * summary: Login user * tags: [Authentication] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - email * - password * properties: * email: * type: string * format: email * example: john@example.com * password: * type: string * format: password * example: password123 * responses: * 200: * description: Login successful * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * example: true * token: * type: string * data: * $ref: '#/components/schemas/User' * 401: * description: Invalid credentials * content: * application/json: * schema: * $ref: '#/components/schemas/Error' */ router.post('/login', loginUser) /** * @swagger * /api/auth/profile: * get: * summary: Get current user profile * tags: [Authentication] * security: * - bearerAuth: [] * responses: * 200: * description: User profile retrieved successfully * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * example: true * data: * $ref: '#/components/schemas/User' * 401: * description: Unauthorized * content: * application/json: * schema: * $ref: '#/components/schemas/Error' */ router.get('/profile', authenticate, getProfile) module.exports = router
Setup Swagger ใน App
// app.js const express = require('express') const { specs, swaggerUi } = require('./config/swagger') const app = express() // Swagger documentation app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs, { explorer: true, customCss: '.swagger-ui .topbar { display: none }' })) // Routes app.use('/api/auth', require('./routes/auth')) app.use('/api/posts', require('./routes/posts')) module.exports = app
Test Scripts และ Coverage
# รัน tests npm test # รัน tests แบบ watch mode npm run test:watch # รัน tests พร้อม coverage report npm run test:coverage
CI/CD Integration
# .github/workflows/test.yml name: Tests on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: [16.x, 18.x, 20.x] steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} cache: 'npm' - run: npm ci - run: npm run test:coverage - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: file: ./coverage/lcov.info
Best Practices
- Test Organization - แยก unit tests และ integration tests
- Test Data - ใช้ factories หรือ fixtures สำหรับ test data
- Mocking - mock external services และ dependencies
- Coverage - มี test coverage อย่างน้อย 80%
- Documentation - อัปเดต API docs เมื่อมีการเปลี่ยนแปลง
- Automation - รัน tests ใน CI/CD pipeline
สรุป
การทดสอบและ documentation ที่ดีช่วยให้ Express.js APIs มีคุณภาพสูง maintainable และใช้งานง่าย ด้วย Jest, Supertest และ Swagger เราสามารถสร้าง comprehensive testing suite และ interactive API documentation ได้อย่างมีประสิทธิภาพ