ถ้าคุณเขียน Software ในระดับ Production มาบ่อยๆ ก็น่าจะได้รู้จักกับคำว่า TDD (Test Driven Development) กันบ้างแล้ว แต่ถ้าไม่เคยรู้จักมาก่อนเดี๋ยวจะมาอธิบายให้ฟัง
เวลาเราเขียน Software ที่มีขนาดใหญ่ขึ้นเรื่อยๆ การ Maintain โค๊ดให้ใช้งานได้ทั้งหมดก็เริ่มยากขึ้นเรื่อยๆ แล้วเวลาเราเขียนโค๊ดไฟล์นึง เราอาจจะไปทำให้ไฟล์อื่นไม่สามารถใช้งานได้แบบเมื่อก่อนได้ เราก็จะต้องมาทำการทดสอบมันใหม่ตั้งแต่ต้นเรื่อยๆ โดย Process นี้ถ้าทำด้วยมือมันจะเป็นขั้นตอนที่ยุ่งยาก และเสียเวลาชีวิตมากๆ ยิ่ง Software ใหญ่ขึ้นเรื่อยๆ ก็ต้องมี Test ที่ใหญ่ขึ้นเรื่อยๆ (อย่างงาน Private เราไปถึง 2000 เทสแล้ว) ก็ถ้าจะทดสอบด้วยมือเองคงไม่น่าไหวมั้ง 555
เราจึงต้องมี TDD เข้ามาช่วยโดยแทนที่เราจะมาทดสอบเอง เราก็ให้ Software มาทดสอบให้เองซะเลย โดยใน Blog วันนี้เราจะพามาทำ TDD ให้กับ REST API ที่เขียนกันใน Express กันนะครับ
ตัวอย่างโค๊ดที่จะเขียนให้ดูอยู่ที่ GitHub นะจ๊ะ กดตรงนี้เลย
ขั้นตอนที่ 1: ติดตั้ง Mocha และ Chai
ก็เพิ่มพวกนี้เข้าไปเป็น Development Dependencies ได้เลย
yarn add mocha chai chai-http --dev
ขั้นตอนที่ 2: อย่าลืม module.exports server ตัวเอง
คือเราใช้ mocha เป็นตัวทดสอบ แต่เราเอา chai chai-http มาทดสอบกับ REST API ของเรา แต่ chai จะหา Express server ไม่เจอเลยถ้าเราไม่ export
เอาไว้ เพราะฉะงั้นก็อย่าลืม export กันด้วย
ใน ES6 จะเป็น export default
ตามตัวอย่างข้างล่างเลย
import bodyParser from 'body-parser'
import chalk from 'chalk'
import express from 'express'
import cors from 'cors'
const {PORT = 3000} = process.env
const server = express()
server.use(bodyParser.json())
server.use(bodyParser.urlencoded({extended: true}))
server.use(cors())
server.get('/', (req, res) => {
res.status(200).send({
message: 'OK',
})
})
server.post('/', (req, res) => {
res.status(200).send({
message: 'OK with POST',
})
})
server.all('*', (req, res) => {
res.status(404).send({
message: 'not found',
})
})
server.listen(PORT, () => {
console.log(`${chalk.black.bgGreen(' INFO ')} app is running on port ${PORT}`)
})
export default server
ขั้นตอนที่ 3: เขียนตัว Test กันเลย
ไฟล์นี่จะเขียนอยู่ใน /test
นะ เดี๋ยวมาอธิบายโค๊ดให้ฟัง
const chai = require('chai')
const chaiHttp = require('chai-http')
const {after, before, describe, it} = require('mocha')
const server = require('../build/main').default
chai.use(chaiHttp)
chai.should()
describe('Testing unit 1', () => {
before(done => {
// Do something here before test
done()
})
describe('GET /', () => {
it('it should have message OK', done => {
chai
.request(server)
.get('/')
.end((e, res) => {
res.should.have.status(200)
res.body.should.have.property('message').eql('OK')
done()
})
})
})
describe('POST /', () => {
it('it should have message OK with POST', done => {
chai
.request(server)
.post('/')
.set('Authorization', 'WHATTTTT')
.send({
data: 'OK',
})
.end((e, res) => {
res.should.have.status(200)
res.body.should.have.property('message').eql('OK with POST')
done()
})
})
})
after(done => {
// Do something here after test
done()
})
})
const server = require('../build/main').default
เวลาเรา Build server มาจาก ES6 จะได้ไฟล์อยู่ที่ build/main.js
เราก็ import ตัวนั้นมาไว้ในตัวแปร server
ย้ำตรงนี้เลยว่าถ้าเขียน JS ปกติใช้ module.exports
แค่ require('../build/main')
ก็พอ แต่นี่เราคือใช้ ES6 มัน export ไว้ที่ default ก็เลยต้อง .default
ตามเข้าไปด้วย
chai.use(chaiHttp)
chai.should()
ส่วนนี้เราจะเอามา Intitialize chai ให้มันทำงาน
describe('Testing unit 1', () => {
// Code...
})
ตัว describe()
เราเอาไว้รวบหลายๆ Test ไว้เป็นกลุ่ม ก็สามารถรวมเป็นกลุ่มซ้อนกันได้กี่ชั้นก็ได้
before(done => {
// Do something here before test
done()
})
บางกรณีก่อนที่เราจะทำอะไรสักอย่างก่อนที่จะเริ่ม Test เช่น แก้ไขข้อมูลบางส่วนบน Database เราก็สามารถทำที่ before()
ได้แล้วก็ใช้ done()
ในการจบด้วยนะ เดี๋ยวค้างไม่ออก
มันก็ไม่ได้มีแค่นี้หรอกมันจะมี
after()
ที่รันตอนสุดท้ายbeforeEach()
จะรันทุกครั้งก่อนที่จะเริ่มทุก TestafterEach()
จะรันทุกครั้งหลัง Test เสร็จ
it('it should have message OK', done => {
// Code...
})
ตัว Test case จริงเราจะทำกันใน it()
แล้วก็ใช้ done()
ในการจบเช่นกัน
chai
.request(server)
.get('/')
.end((e, res) => {
res.should.have.status(200)
res.body.should.have.property('message').eql('OK')
done()
})
คราวนี้ได้ใช้งาน chai จริงๆล่ะ
เราก็เริ่มจากดึง Server มาโดยใช้ .request(server)
แล้วก็เรียก path ว่าจะเรียก path ไหน method อะไร .get('/')
แล้วบางกรณีสำหรับ POST อะพวกนี้อาจจะต้องส่ง payload หรือตั้ง header ก็สามารถใช้ .set()
, .send()
ได้ตามตัวอย่าง
จากนั้นก็เรียก request โดยใช้ .end()
แล้วก็เอาของข้างในมาเช็คต่อ ในตัวอย่างก็จะเป็น
- ดูว่าส่ง HTTP Status มาเป็น 200 หรือไม่ (
res.should.have.status(200)
) - ส่วน response ที่ออกมามี message ว่า OK หรือไม่ (
res.body.should.have.property('message').eql('OK')
)
ถ้าไม่ผ่านก็จะ fail ไปแต่ถ้าผ่านทุกกรณีก็จะให้ผ่าน done()
ออกไปนั่นเอง
ขั้นตอนที่ 4: Test แม่ม
เราแนะนำให้แนบคำสั่งไว้ใน script บน package.json
ดีกว่านะ
{
"license": "MIT",
"dependencies": {
"backpack-core": "^0.8.3",
"body-parser": "^1.18.3",
"chalk": "^2.4.2",
"cors": "^2.8.5",
"express": "^4.16.4"
},
"devDependencies": {
"chai": "^4.2.0",
"chai-http": "^4.2.1",
"mocha": "^6.0.0"
},
"scripts": {
"build": "backpack build",
"dev": "backpack",
"start": "node ./build/main.js",
"test": "yarn build && mocha ./test --exit"
}
}
yarn test
แล้ว mocha ก็จะรันเช็คทดสอบทุกอย่างให้ตามภาพด้านล่างเลย
Testing unit 1
GET /
√ it should have message OK (52ms)
1 passing (80ms)
แล้วถ้าเราใช้ Git กันอยู่แล้วก็สามารถให้ Travis CI ทดสอบให้ด้วยก็ยังได้...ง่าย และสบายไปอีกขั้น
language: node_js
os: linux
node_js:
- "lts/*"
cache:
yarn: true
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.13.0
- export PATH=$HOME/.yarn/bin:$PATH
script:
- yarn test
สรุป
เท่านี้ก็ได้ Basic แล้ว ที่เหลือคือก็มาคิดดูว่าเราจะเอาของพวกนี้ไปใช้กันยังไงแล้ว ส่วนข้อมูลเพิ่มเติมต่างๆเดี๋ยวจะแปะเอาไว้ให้ด้านล่าง แล้วเดี๋ยวต่อไปเราจะมาสอบใช้งาน Puppeteer กัน แล้วเจอกันครับ :)