Back

พื้นฐานการทดสอบ RESTful API (Express + MySQL) ด้วย JestBlur image

บทความนี้จะแนะนำการเขียนและรันการทดสอบ (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
bash

3.1 ติดตั้ง Dependencies#

# ติดตั้ง Production Dependencies
npm install express body-parser cors @prisma/client

# ติดตั้ง Development Dependencies  
npm install -D jest supertest prisma
bash

3.2 โครงสร้างไฟล์#

api-testing-demo/
├── prisma/
│   └── schema.prisma
├── src/
│   └── prisma.js
├── tests/
│   └── api.test.js
├── index.js
├── package.json
└── .env
plaintext

4. ตั้งค่าฐานข้อมูลและ Prisma#

4.1 สร้างไฟล์ .env#

DATABASE_URL="mysql://username:password@localhost:3306/test_db"
plaintext

4.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?
}
prisma

4.3 สร้าง Prisma Client#

สร้างไฟล์ src/prisma.js:

const { PrismaClient } = require('@prisma/client');

const prisma = new PrismaClient();

module.exports = prisma;
javascript

4.4 สร้างฐานข้อมูล#

npx prisma migrate dev --name init
npx prisma generate
bash

5. สร้าง 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 Client
  • prisma.user.findMany() - ดึงข้อมูลผู้ใช้ทั้งหมด
  • prisma.user.findUnique() - ดึงข้อมูลผู้ใช้ตาม ID
  • prisma.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');
  });
});
javascript

7. คำอธิบาย Test Cases แต่ละส่วน#

7.1 การนำเข้า Dependencies#

const request = require('supertest');
const app = require('../index');
javascript
  • supertest - สำหรับส่ง 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
});
javascript
  • describe() - จัดกลุ่ม 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');
});
javascript
  • it() - กำหนด Test Case เดี่ยว
  • request(app).get('/') - ส่ง GET Request ไปยัง Root Path
  • expect(res.statusCode).toBe(200) - ตรวจสอบ Status Code
  • expect(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 Body
  • Date.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()เป็น nullexpect(value).toBeNull()
.toBeTruthy()เป็น truthyexpect(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 Report
  • testTimeout: 10000 - ตั้งค่า Timeout เป็น 10 วินาที (สำหรับ Database Operations)

9. รันการทดสอบ#

9.1 เริ่มต้นฐานข้อมูล#

# สร้างฐานข้อมูลและตาราง
npx prisma migrate dev --name init

# สร้าง Prisma Client
npx prisma generate
bash

9.2 รันการทดสอบ#

# รันการทดสอบครั้งเดียว
npm test

# รันการทดสอบแบบ Watch Mode
npm run test:watch

# รันการทดสอบพร้อม Coverage Report
npm run test:coverage
bash

9.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.
plaintext

10. การทดสอบ 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.
plaintext

11. 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 เป็นส่วนสำคัญของการพัฒนาซอฟต์แวร์สมัยใหม่ เครื่องมือเหล่านี้จะช่วยให้การพัฒนาเป็นไปอย่างมีคุณภาพและเชื่อถือได้

พื้นฐานการทดสอบ RESTful API (Express + MySQL) ด้วย Jest
ผู้เขียน กานต์ ยงศิริวิทย์ / Karn Yongsiriwit
เผยแพร่เมื่อ July 20, 2025
ลิขสิทธิ์ CC BY-NC-SA 4.0

กำลังโหลดความคิดเห็น...

ความคิดเห็น 0