การทดสอบการทำงานฟังก์ชันต่าง ๆ ของโปรแกรมว่าสามารถทำงานได้ตามที่คาดหวังหรือไม่ มีความสำคัญเป็นอย่างมาก เพื่อให้มั่นใจได้ว่าเมื่อนำฟังก์ชันต่าง ๆ มาทำงานร่วมกันแล้ว ทุกอย่างจะสามารถทำงานได้โดยไม่เกิดความผิดพลาดใด ๆ ซึ่ง Jest เป็น Framework หนึ่งที่ช่วยให้สามารถทำการตรวจสอบการทำงานของฟังก์ชัน ๆ ได้อย่างมีประสิทธิภาพ สามารถศึกษาการใช้งาน Jest เบื้องต้นได้จากบทความนี้ ทำ Unit Test เบื้องต้นบน Node.Js ด้วย Jest
พื้นฐานที่ต้องมีก่อนจะทำตามบทความนี้
- Jest ควรเข้าใจฟังก์ชันพื้นฐานของ Jest และการใช้งาน Jest เบื้องต้น
- การสร้าง API สำหรับการลงทะเบียนและการเรียกรู้ข้อมูลผู้ใช้งานในระบบ
เริ่มต้น Setup โปรเจคกันเลย
ขั้นแรกก็ทำการสร้างโปรเจคโดยใช้คำสั่ง npm init ใน Terminal จากนั้นทำการติดตั้ง express jest และ supertest โดยใช้คำส่ง npm install express jest supertest ซึ่งเมื่อติดตั้งเสร็จแล้วในไฟล์ package.json ควรจะปรากฎ Library ที่ลงไว้ ดังรูป
จากรูปด้านบนจะเห็นว่า ในโค้ดบรรทัดที่ 7 ส่วนของ “test” ได้กำหนดค่าเป็น “jest” เพื่อที่จะสามารถใช้คำสั่ง npm test ใน Terminal ได้ เมื่อจะทำการทดสอบโดยใช้ Jest
จากนั้นสร้างไฟล์ app.js โดยมีโค้ดดังนี้
const express = require("express");
function makeApp(database){
const app = express();
app.use(express.json())
app.post("/", async (req, res)=> {
const {username, password} = req.body;
if (!password || !username) {
res.sendStatus(400)
return
}
const newUser = await database.createUser(username,password);
res.status(200).json(newUser)
})
return app
}
module.exports = makeApp
จากโค้ดใน app.js จะเป็นการสร้างฟังก์ชันชื่อ makeApp ซึ่งรับตัวแปรชื่อ database การเขียนในลักษณะนี้ทำให้สามารถเปลี่ยนฐานข้อมูลที่จะนำมาใช้กับ app ได้ง่าย ซึ่งจะทำให้สะดวกในการทำการทดสอบการทำงานของ API ของเรา
app ที่สร้างขึ้นภายในฟังก์ชันนี้ จะรอบรับวิธีการ POST ซึ่งต้องการข้อมูล username และ password โดยจะมีการตรวจสอบว่าผู้ใช้งานได้กรอกข้อมูล username และ password หรือไม่ หากขาดข้อมูลใดข้อมูลหนึ่งจะส่ง status code เป็น 400 กลับไป แต่หากมีข้อมูลครบถ้วน จะเรียกใช้ฟังก์ชัน createUser ของ database โดยส่งข้อมูล username และ password เข้าไป เพื่อบันทึกข้อมูลในฐานข้อมูล และจะส่ง Status Code เป็น 200 และ newUser ซึ่งเป็นข้อมูลที่ถูกส่งกลับมาจากฐานข้อมูลไปให้ผู้ใช้งาน
ในการใช้งานจริง database จะเป็นส่วนที่เกี่ยวข้องกับฐานข้อมูลที่ใช้งาน เช่นการเชื่อม การใช้ SQL เพื่อเพิ่มข้อมูลในตารางของฐานข้อมูล และการส่งข้อมูลจากฐานข้อมูลกลับไปที่ app ดังตัวอย่างโค้ดด้านล่างนี้
const {Pool} = require('pg')
const pool = new Pool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
port: 5432,
database: process.env.DB_NAME
})
const createUser= (username,password)=>{
query_createUser = 'INSERT INTO userstable (username, password) VALUES($1, $2) returning *; '
pool.query(query_createUser,[username,password],(error,results)=>{
if(error) throw error;
return results.rows[0]
});
};
database = {};
database.createUser = createUser;
module.exports = database
จะเห็นว่าในโค้ดตัวอย่างนี้ database จะมีฟังก์ชันชื่อ createUser ซึ่งจะทำหน้าที่ในการนำข้อมูล username และ password ไปเพิ่มในฐานข้อมูล และส่งข้อมูลที่พึ่งเพิ่มกลับไปให้ app (ข้อมูลที่ส่งกลับไปให้ app นอกจาก username และ password ยังมี id ซึ่งเป็น Primary Key อีกด้วย) ซึ่งในบทความนี้ จะใช้ jest.fn() เพื่อสร้างฟังก์ชันจำลองแทนฟังก์ชัน createUser นี้
สร้างไฟล์สำหรับทดสอบ app.js ด้วย jest
สร้างไฟล์ app.test.js ที่ใช้ในการทดสอบการทำงานของ app.js จากนั้นเขียนโค้ดดังนี้
const request = require("supertest");
const makeApp = require("./app.js");
const mockCreateUser = jest.fn();
const mockDatabase = {};
mockDatabase.createUser = mockCreateUser;
const app = makeApp(mockDatabase);
โค้ดข้างต้นเป็นการเรียนใช้ supertest และฟังก์ชัน makeApp ที่เราพึ่งเขียนไว้ใน app.js จากนั้นสร้างฟังก์ชันจำลองชื่อ mockCreateUser โดยใช้ jest.fn() และสร้าง Object ชื่อ mockDatabase ซึ่งจะมี Property ชื่อ createUser ที่ตรงกับที่เราจะเรียกใช้ใน app.js
จากสร้าง Block สำหรับทำการตรวจสอบการทำงานของฟังก์ชัน createUser
test("should save the username and password to the database", async () => {
const bodyData = { username: "username1", password: "password1"}
const response = await request(app).post("/").send(bodyData);
});
bodyData เป็นข้อมูลทดสอบและ response เป็นค่าที่ส่งกลับมาหาผู้ใช้งานจาก app ภายหลังผู้ใช้ลงทะเบียนใน app โดยในการทดสอบนี้ app จะเรียกใช้ฟังก์ชันจำลอง mockCreateUser
เราสามารถตรวจสอบได้ว่า username และ password ที่เราส่งเข้าไปใน app ผ่านเข้าไปยังฟังก์ชันถูกต้องหรือไม่ โดย
test("should save the username and password to the database", async () => {
const bodyData = { username: "username1", password: "password1"}
const response = await request(app).post("/").send(bodyData);
expect(mockCreateUser.mock.calls[0][0]).toBe(bodyData.username);
expect(mockCreateUser.mock.calls[0][1]).toBe(bodyData.password);
});
mockCreateUser.mock.calls[0][0] คือ Parameter ลำดับที่ 1 ที่ส่งเข้าไปที่ฟังก์ชัน mockCreateUser เมื่อฟังก์ชันนี้ถูกเรียกครั้งแรก
mockCreateUser.mock.calls[0][1] คือ Parameter ลำดับที่ 2 ที่ส่งเข้าไปที่ฟังก์ชัน mockCreateUser เมื่อฟังก์ชันนี้ถูกเรียกครั้งแรก
หากเราลองทดสอบการทำงานโดยพิมพ์คำสั่ง npm test ใน terminal ควรจะได้ผลว่า PASS ดังนี้
สมมติว่าใน app.js เราลืมส่งค่า password เข้าไปในฟังก์ชัน createUser ดังนี้
เมื่อเราลองทำการทดสอบการทำงานอีกครั้ง ควรจะได้ผลเป็น Fail และมีข้อความแจ้งเตือนว่า ค่าที่ฟังก์ชันควรจะได้รับคือ “password1” แต่ส่งที่ส่งเข้าไปในฟังก์ชันกลับเป็น undefined ดังรูป
ถัดมาเราจะตรวจสอบขั้นต่อไปว่า เมื่อฐานข้อมูลส่งข้อมูลกลับมา app สามารถรับข้อมูลและส่งกับไปให้ผู้ใช้งานได้อย่างถูกต้องหรือไม่ แต่เนื่องจากตอนนี้เราใช้ฟังก์ชันจำลอง ซึ่งไม่ได้เชื่อมต่อกับฐานข้อมูลจริงๆ ดังนั้นเราจะต้องเพิ่มโค้ดเพื่อให้ฟังก์ชันจำลอง mockCreateUser ส่งข้อมูล id username และ password กลับมาเมื่อถูกเรียกใช้ เพื่อเลียนแบบการส่งข้อมูลกลับจากฐานข้อมูล โดย id กำหนดให้เป็น 1 ค่า ซึ่งในการทำงานจริงค่า id นี้ ฐานข้อมูลจะจัดการเพิ่มในตารางให้โดยอัตโนมัติ username และ password กำหนดให้มีค่าเหมือนกับ input ที่ส่งเข้าไปในฟังก์ชัน ดังนี้
ข้อมูลที่ผู้ใช้งานจะได้รับถูกเก็บไว้ใน response ซึ่งนอกจากจะมีค่า id username และ password แล้ว ควรจะมีค่า status code เป็น 200 อีกด้วย ทำการเพิ่มการตรวจสอบได้ดังนี้
test("should save the username and password to the database", async () => {
const bodyData = { username: "username1", password: "password1"}
//ให้ mockCreateUser ส่งข้อมูล id usrname และ password กลับไปเมื่อถูกเรียกใช้งาน
mockCreateUser.mockResolvedValue({ id: 1, ...bodyData });
const response = await request(app).post("/").send(bodyData);
expect(mockCreateUser.mock.calls[0][0]).toBe(bodyData.username);
expect(mockCreateUser.mock.calls[0][1]).toBe(bodyData.password);
expect(response.statusCode).toBe(200); // ตรวจสอบว่า status code = 200
expect(response.body.id).toBe(1); // ตรวจสอบว่า id = 1
expect(response.body.username).toBe(bodyData.username);
expect(response.body.password).toBe(bodyData.password);
});
เมื่อทดสอบการทำงานโดยพิมพ์คำสั่ง npm test ใน Terminal ควรจะได้ผลว่า PASS ดังนี้
สรุป
การใช้งาน Jest สามารถใช้ในการสร้างฟังก์ชันจำลองซึ่งสามารถตรวจสอบ Parameter ต่าง ๆ ที่ถูกส่งเข้าฟังก์ชันจำลองนี้ และยังสามารถกำหนดค่าตัวแปรต่าง ๆ ที่จะส่งออกมาเพื่อฟังก์ชันจำลองถูกใช้งาน ซึ่งคุณสมบัติเหล่านี้มีประโยชน์เป็นอย่างมากในการตรวจสอบการทำงานของ API แบบแยกส่วน ซึ่งจะง่ายต่อการหาสาเหตุเมื่อเกิดความผิดพลาดขึ้นในระบบ
อ้างอิง
- https://www.borntodev.com/2022/06/24/%e0%b8%97%e0%b8%b3-unit-test-%e0%b8%9a%e0%b8%99-node-%e0%b8%94%e0%b9%89%e0%b8%a7%e0%b8%a2-jest/
- https://jestjs.io/