

บทความนี้จะแนะนำการเขียนและรันการทดสอบ (Unit Test) สำหรับ RESTful API ที่ใช้ Node.js, Express, Prisma ORM และ MySQL โดยใช้เครื่องมือทดสอบ Jest และ Supertest
1. วัตถุประสงค์#
- เรียนรู้การติดตั้งและตั้งค่า Jest และ Supertest
- เข้าใจหลักการเขียน Unit Test สำหรับ API
- ทดสอบการทำงานของ CRUD Operations
- ตรวจสอบ HTTP Status Code และ Response Data
- รันการทดสอบแบบอัตโนมัติ
2. เครื่องมือที่ใช้ในการทดสอบ#
2.1 Jest#
Jest เป็น JavaScript Testing Framework ที่พัฒนาโดย Facebook
ข้อดี:
- ใช้งานง่าย ไม่ต้องตั้งค่าซับซ้อน
- มี Assertion Library ในตัว
- รองรับ Async/Await
- แสดงผลการทดสอบที่อ่านง่าย
- มี Code Coverage Report
2.2 Supertest#
Supertest เป็น Library สำหรับทดสอบ HTTP Server
ข้อดี:
- ทำงานร่วมกับ Express ได้ดี
- รองรับการส่ง HTTP Request ทุกประเภท
- ตรวจสอบ Response ได้ครบถ้วน
- เขียน Test Case ได้อย่างเป็นระบบ
3. โครงสร้างโปรเจค#
สร้างโปรเจคใหม่และติดตั้ง Dependencies
mkdir api-testing-demo
cd api-testing-demo
npm init -y
bash3.1 ติดตั้ง Dependencies#
# ติดตั้ง Production Dependencies
npm install express body-parser cors @prisma/client
# ติดตั้ง Development Dependencies
npm install -D jest supertest prisma
bash3.2 โครงสร้างไฟล์#
api-testing-demo/
├── prisma/
│ └── schema.prisma
├── src/
│ └── prisma.js
├── tests/
│ └── api.test.js
├── index.js
├── package.json
└── .env
plaintext4. ตั้งค่าฐานข้อมูลและ Prisma#
4.1 สร้างไฟล์ .env#
DATABASE_URL="mysql://username:password@localhost:3306/test_db"
plaintext4.2 ตั้งค่า Prisma Schema#
สร้างไฟล์ prisma/schema.prisma
:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
fname String
lname String
username String @unique
email String @unique
avatar String?
}
prisma4.3 สร้าง Prisma Client#
สร้างไฟล์ src/prisma.js
:
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
module.exports = prisma;
javascript4.4 สร้างฐานข้อมูล#
npx prisma migrate dev --name init
npx prisma generate
bash5. สร้าง API Server#
สร้างไฟล์ index.js
:
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const prisma = require('./src/prisma');
const app = express();
const port = 5000;
// Middleware
app.use(cors());
app.use(bodyParser.json());
// Routes
app.get('/', (req, res) => {
res.send('Hello! RESTful API is ready to use with Prisma')
});
// Get all users
app.get('/users', async (req, res) => {
const users = await prisma.user.findMany();
res.json(users);
});
// Get user by ID
app.get('/users/:id', async (req, res) => {
const id = parseInt(req.params.id);
const user = await prisma.user.findUnique({ where: { id } });
if (!user) return res.status(404).json({ message: 'User not found' });
res.json(user);
});
// Create new user
app.post('/users', async (req, res) => {
try {
const newUser = await prisma.user.create({ data: req.body });
res.status(201).json(newUser);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// Update user
app.put('/users/:id', async (req, res) => {
try {
const id = parseInt(req.params.id);
const updated = await prisma.user.update({
where: { id },
data: req.body
})
res.json(updated);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// Delete user
app.delete('/users/:id', async (req, res) => {
try {
const id = parseInt(req.params.id);
const deleted = await prisma.user.delete({ where: { id } });
res.json({ message: `User with ID ${deleted.id} deleted` })
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// Start server only when run directly (not in tests)
if (require.main === module) {
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
}
// Export for testing
module.exports = app;
javascriptคำอธิบายโค้ดสำคัญ:
require('./src/prisma')
- นำเข้า Prisma Clientprisma.user.findMany()
- ดึงข้อมูลผู้ใช้ทั้งหมดprisma.user.findUnique()
- ดึงข้อมูลผู้ใช้ตาม IDprisma.user.create()
- สร้างผู้ใช้ใหม่prisma.user.update()
- อัปเดตข้อมูลผู้ใช้prisma.user.delete()
- ลบผู้ใช้if (require.main === module)
- เริ่มเซิร์ฟเวอร์เฉพาะเมื่อรันไฟล์โดยตรงmodule.exports = app
- Export แอปพลิเคชันสำหรับการทดสอบ
6. เขียน Test Cases#
สร้างไฟล์ tests/api.test.js
:
const request = require('supertest');
const app = require('../index');
describe('Prisma API Tests', () => {
let createdUserId;
// Test 1: Test Root Endpoint
it('GET / should return API ready message', async () => {
const res = await request(app).get('/');
expect(res.statusCode).toBe(200);
expect(res.text).toContain('RESTful API is ready');
});
// Test 2: Test creating a user
it('POST /users should create a user', async () => {
const res = await request(app)
.post('/users')
.send({
fname: 'John',
lname: 'Smith',
username: `john_smith_${Date.now()}`, // unique username
email: `john${Date.now()}@example.com`, // unique email
avatar: 'https://example.com/avatar.jpg'
});
expect(res.statusCode).toBe(201);
expect(res.body).toHaveProperty('id');
createdUserId = res.body.id; // Save ID for later tests
});
// Test 3: Test retrieving the created user
it('GET /users/:id should return the created user', async () => {
const res = await request(app).get(`/users/${createdUserId}`);
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('fname', 'John');
expect(res.body).toHaveProperty('lname', 'Smith');
});
// Test 4: Test updating the user
it('PUT /users/:id should update the user', async () => {
const res = await request(app)
.put(`/users/${createdUserId}`)
.send({ fname: 'Jackson', lname: 'Mars' });
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('fname', 'Jackson');
expect(res.body).toHaveProperty('lname', 'Mars');
});
// Test 5: Test deleting the user
it('DELETE /users/:id should delete the user', async () => {
const res = await request(app).delete(`/users/${createdUserId}`);
expect(res.statusCode).toBe(200);
expect(res.body.message).toContain(`User with ID ${createdUserId} deleted`);
});
// Test 6: Test retrieving a deleted user
it('GET /users/:id should return 404 for deleted user', async () => {
const res = await request(app).get(`/users/${createdUserId}`);
expect(res.statusCode).toBe(404);
expect(res.body).toHaveProperty('message', 'User not found');
});
});
javascript7. คำอธิบาย Test Cases แต่ละส่วน#
7.1 การนำเข้า Dependencies#
const request = require('supertest');
const app = require('../index');
javascriptsupertest
- สำหรับส่ง HTTP Request ไปยัง Express App../index
- นำเข้า Express Application ที่จะทดสอบ
7.2 Test Suite Structure#
describe('Prisma API Tests', () => {
let createdUserId; // Variable to store the ID of the created user
// Test cases will be here
});
javascriptdescribe()
- จัดกลุ่ม Test Cases ที่เกี่ยวข้องกันcreatedUserId
- เก็บ ID ของข้อมูลที่สร้างขึ้นเพื่อใช้ในการทดสอบต่อ
7.3 การทดสอบแต่ละ Endpoint#
Test 1: Root Endpoint
it('GET / should return API ready message', async () => {
const res = await request(app).get('/');
expect(res.statusCode).toBe(200);
expect(res.text).toContain('RESTful API is ready');
});
javascriptit()
- กำหนด Test Case เดี่ยวrequest(app).get('/')
- ส่ง GET Request ไปยัง Root Pathexpect(res.statusCode).toBe(200)
- ตรวจสอบ Status Codeexpect(res.text).toContain()
- ตรวจสอบเนื้อหาใน Response
Test 2: การสร้างผู้ใช้
it('POST /users should create a user', async () => {
const res = await request(app)
.post('/users')
.send({
fname: 'John',
lname: 'Smith',
username: `john_smith_${Date.now()}`,
email: `john${Date.now()}@example.com`,
avatar: 'https://example.com/avatar.jpg'
});
expect(res.statusCode).toBe(201);
expect(res.body).toHaveProperty('id');
createdUserId = res.body.id;
});
javascript.send()
- ส่งข้อมูลใน Request BodyDate.now()
- สร้าง Unique Value เพื่อหลีกเลี่ยงข้อมูลซ้ำtoHaveProperty('id')
- ตรวจสอบว่า Response มี Property นั้น- เก็บ
id
ไว้ใช้ในการทดสอบต่อไป
7.4 Jest Matchers ที่ใช้บ่อย#
Matcher | ความหมาย | ตัวอย่าง |
---|---|---|
.toBe() | เท่ากับ (===) | expect(status).toBe(200) |
.toEqual() | เท่ากับ (deep equality) | expect(obj).toEqual({id: 1}) |
.toContain() | มีข้อความ/ค่านั้น | expect(text).toContain('success') |
.toHaveProperty() | มี Property นั้น | expect(obj).toHaveProperty('name') |
.toBeNull() | เป็น null | expect(value).toBeNull() |
.toBeTruthy() | เป็น truthy | expect(result).toBeTruthy() |
8. ตั้งค่า NPM Scripts#
เพิ่มใน package.json
:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"start": "node index.js"
},
"jest": {
"testEnvironment": "node",
"testTimeout": 10000
}
}
jsonคำอธิบาย Scripts:
npm test
- รันการทดสอบทั้งหมดnpm run test:watch
- รันการทดสอบและติดตามการเปลี่ยนแปลงไฟล์npm run test:coverage
- รันการทดสอบพร้อม Code Coverage ReporttestTimeout: 10000
- ตั้งค่า Timeout เป็น 10 วินาที (สำหรับ Database Operations)
9. รันการทดสอบ#
9.1 เริ่มต้นฐานข้อมูล#
# สร้างฐานข้อมูลและตาราง
npx prisma migrate dev --name init
# สร้าง Prisma Client
npx prisma generate
bash9.2 รันการทดสอบ#
# รันการทดสอบครั้งเดียว
npm test
# รันการทดสอบแบบ Watch Mode
npm run test:watch
# รันการทดสอบพร้อม Coverage Report
npm run test:coverage
bash9.3 ผลลัพธ์การทดสอบที่คาดหวัง#
PASS tests/api.test.js
Prisma API Tests
√ GET / should return API ready message (26 ms)
√ POST /users should create a user (154 ms)
√ GET /users/:id should return the created user (20 ms)
√ PUT /users/:id should update the user (13 ms)
√ DELETE /users/:id should delete the user (8 ms)
√ GET /users/:id should return 404 for deleted user (6 ms)
Test Suites: 1 passed, 1 total
Tests: 6 passed, 6 total
Snapshots: 0 total
Time: 0.829 s, estimated 1 s
Ran all test suites.
plaintext10. การทดสอบ Error Cases เพิ่มเติม#
เพิ่ม Test Cases สำหรับกรณีข้อผิดพลาด:
describe('Error Handling Tests', () => {
// Test creating a user with incomplete data
it('POST /users should return 500 for invalid data', async () => {
const res = await request(app)
.post('/users')
.send({
fname: 'John'
// Missing lname, username, email
});
expect(res.statusCode).toBe(500);
expect(res.body).toHaveProperty('message');
});
// Test retrieving a user that does not exist
it('GET /users/999999 should return 404', async () => {
const res = await request(app).get('/users/999999');
expect(res.statusCode).toBe(404);
expect(res.body).toHaveProperty('message', 'User not found');
});
// Test creating a user with a duplicate email
it('POST /users should return 500 for duplicate email', async () => {
const userData = {
fname: 'Jane',
lname: 'Doe',
username: 'jane_doe_test',
email: 'duplicate@test.com',
avatar: 'https://example.com/avatar.jpg'
};
// Create user the first time
await request(app).post('/users').send(userData);
// Attempt to create another user with the same email
const res = await request(app).post('/users').send({
...userData,
username: 'jane_doe_test2' // Change username but keep the same email
});
expect(res.statusCode).toBe(500);
expect(res.body.message).toContain('email');
});
});
javascriptผลลัพธ์การทดสอบที่คาดหวัง
PASS tests/api.test.js
Prisma API Tests
√ GET / should return API ready message (30 ms)
√ POST /users should create a user (28 ms)
√ GET /users/:id should return the created user (7 ms)
√ PUT /users/:id should update the user (8 ms)
√ DELETE /users/:id should delete the user (8 ms)
√ GET /users/:id should return 404 for deleted user (5 ms)
Error Handling Tests
√ POST /users should return 500 for invalid data (133 ms)
√ GET /users/999999 should return 404 (6 ms)
√ POST /users should return 500 for duplicate email (17 ms)
Test Suites: 1 passed, 1 total
Tests: 9 passed, 9 total
Snapshots: 0 total
Time: 0.686 s, estimated 1 s
Ran all test suites.
plaintext11. Code Coverage#
การตรวจสอบ Code Coverage เพื่อดูว่าโค้ดถูกทดสอบครอบคลุมเท่าไหร่:
npm run test:coverage
bashผลลัพธ์ที่แสดง:
PASS tests/api.test.js
Prisma API Tests
√ GET / should return API ready message (25 ms)
√ POST /users should create a user (28 ms)
√ GET /users/:id should return the created user (6 ms)
√ PUT /users/:id should update the user (8 ms)
√ DELETE /users/:id should delete the user (8 ms)
√ GET /users/:id should return 404 for deleted user (6 ms)
Error Handling Tests
√ POST /users should return 500 for invalid data (141 ms)
√ GET /users/999999 should return 404 (5 ms)
√ POST /users should return 500 for duplicate email (16 ms)
----------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------------|---------|----------|---------|---------|-------------------
All files | 86.04 | 75 | 71.42 | 85.71 |
api-prisma | 85 | 75 | 71.42 | 84.61 |
index.js | 85 | 75 | 71.42 | 84.61 | 17-18,46,56,61-62
api-prisma/src | 100 | 100 | 100 | 100 |
prisma.js | 100 | 100 | 100 | 100 |
----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 9 passed, 9 total
Snapshots: 0 total
Time: 0.749 s, estimated 1 s
Ran all test suites.
plaintextคำอธิบาย:
- % Stmts - เปอร์เซ็นต์ของ Statements ที่ถูกทดสอบ
- % Branch - เปอร์เซ็นต์ของ Conditional Branches ที่ถูกทดสอบ
- % Funcs - เปอร์เซ็นต์ของ Functions ที่ถูกทดสอบ
- % Lines - เปอร์เซ็นต์ของบรรทัดโค้ดที่ถูกทดสอบ
12. Best Practices สำหรับการทดสอบ API#
✅ สิ่งที่ควรทำ:
- ใช้ข้อมูลที่ unique ในแต่ละ test (เช่น timestamp)
- ทดสอบทั้ง Success และ Error Cases
- ทำความสะอาดข้อมูลหลังการทดสอบ
- เขียน Test Case ที่อ่านเข้าใจง่าย
- ใช้
describe()
จัดกลุ่ม Test Cases - ตรวจสอบทั้ง Status Code และ Response Data
❌ สิ่งที่ควรหลีกเลี่ยง:
- ใช้ข้อมูลแบบ Hard-coded ที่อาจซ้ำกัน
- เขียน Test Cases ที่ depend กันมากเกินไป
- ไม่ทดสอบ Error Cases
- ใช้ชื่อ Test Case ที่ไม่ชัดเจน
- ไม่ตรวจสอบ Response ที่สมบูรณ์
การทดสอบ API เป็นส่วนสำคัญของการพัฒนาซอฟต์แวร์สมัยใหม่ เครื่องมือเหล่านี้จะช่วยให้การพัฒนาเป็นไปอย่างมีคุณภาพและเชื่อถือได้