01204223/unit testing
- หน้านี้เป็นส่วนหนึ่งของ 01204223
เราจะหัดใช้ Unit testing ในการควบคุมคุณภาพโปรแกรมที่เราเขียน ปกติถ้าเมื่อเราเขียนโปรแกรม เราจะทดสอบว่าโปรแกรมทำงานถูกต้องโดยทดลองรันและตรวจสอบผล แต่เมื่อโปรแกรมใหญ่ขึ้นเรื่อย ๆ เราจะไม่สามารถทดสอบทุก ๆ ส่วนที่เราเขียนมาได้ การพัฒนาระบบตรวจสอบอัตโนมัติเป็นแนวปฏิบัติพื้นฐานในการรักษาคุณภาพซอฟต์แวร์ รวมถึงอาจจะใช้ในการขับเคลื่อนการพัฒนาด้วยก็ได้
การทดสอบระบบมีได้หลายระดับ Unit testing เป็นการทดสอบซอฟต์แวร์ในหน่วยย่อย (unit) เช่น หนึ่งฟังก์ชัน หรือหนึ่ง component (ใน react) ส่วน Integration testing หรือ End-to-end testing เป็นการทดสอบการทำงานของส่วนย่อย ๆ ที่เชื่อมต่อกัน ในที่นี้เราจะเน้นแค่ unit testing ก่อน เพราะว่าเป็นจุดเริ่มต้นที่ดีรวมทั้งมักจะทำได้ง่ายกว่า
ในการพัฒนาเว็บแอพลิเคชัน แต่ละ unit ที่เราต้องการทดสอบมักทำงานร่วมกับส่วนอื่นๆ เช่น ถ้าพิจารณาส่วนหลังบ้าน โค้ดโดยมากก็จะทำงานกับฐานข้อมูล (database) ในขณะที่โค้ดหน้าบ้านก็จะต้องมีการเรียก API ดังนั้นการจะทดสอบแต่ละ unit จะต้องมีการสร้างหรือจำลองส่วนอื่น ๆ ของระบบเพื่อทำให้โค้ดที่เราต้องการทดสอบสามารถทำงานได้ และมีพฤติกรรมที่คงเส้นคงวาทำให้เราสามารถตรวจสอบความถูกต้องได้ เราจะได้ลองใช้เทคนิคที่เรียกว่า mock ในการจำลองการทำงานของส่วนอื่น ๆ ระหว่างที่โค้ดที่เราเทสทำงาน
ทดสอบ React component
สำหรับส่วนหน้าบ้าน เราจะใช้ Vitest เป็นเครื่องมือในการรันเทสสำหรับ React component เราจะติดตั้ง vitest รวมทั้งไลบรารีอื่น ๆ โดยสั่ง
npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event
ในไดเร็กทอรี frontend
ไลบรารีที่เราติดตั้งมีหน้าที่ดังนี้
- vitest เป็นระบบเทส
- jsdom ใช้สำหรับจำลองหน้าจอ (dom) โดยไม่มี browser
- @testing-library/xxx เป็นเครื่องมือต่าง ๆ สำหรับทดสอบ react
เมื่อติดตั้งเสร็จ ให้ไปแก้ไฟล์ package.json เพื่อระบุว่าเมื่อสั่ง test ให้เรียก vitest โดยดูในส่วน "scripts" และเพิ่มบรรทัดดังด้านล่างเข้าไป
// ในไฟล์ frontend/package.json
"scripts": {
// ละไว้
// ** ระหว่างที่เพิ่ม อย่าลืมเติม , ท้ายบรรทัดก่อนหน้าด้วย
"test": "vitest run"
},
เมื่อแก้เสร็จ เราจะสามารถสั่งรันเทสได้โดยสั่ง
npm test
แต่เนื่องจากตอนนี้เราไม่มีเทสเคสเลย vitest จะบอกว่าไม่มี test files และจบการทำงาน (หมายเหตุ: เราสามารถสั่งให้ vitest ดูการแก้ไขไฟล์แล้วรันเทสใหม่ได้ แต่ในที่นี้เราเลือกจะให้รันเทสตอนที่เราสั่งก่อนเท่านั้น)
เราจะต้องไปเพิ่ม config ของ test ในไฟล์ vite.config.js ดังนี้
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
// ** เพิ่มส่วนด้านล่างนี้ **
test: {
globals: true, // ทำให้เรียกฟังก์ชันเกี่ยวกับการเทสได้โดยไม่ต้องประกาศ
environment: 'jsdom', // รันเทสแบบไม่มี browser
setupFiles: './src/setupTests.js', // ระบุโค้ดสำหรับเตรียมต่าง ๆ
},
})
จากนั้นให้เพิ่มไฟล์ src/setupTests.js ดังนี้
import '@testing-library/jest-dom/vitest'
import { afterEach } from 'vitest'
import { cleanup } from '@testing-library/react'
afterEach(() => {
cleanup()
})
โค้ดดังกล่าวจะทำให้มีการเรียกฟังก์ชัน cleanup เมื่อสิ้นสุดการรันในแต่ละเทสเคส
Test แรก
เพื่อความง่ายเราจะเริ่มจากการทดสอบ component TodoItem ก่อน เพราะว่าเป็น component ที่ไม่ได้มีการเรียก API
เราจะเก็บเทสทั้งหมดไว้ในไดเร็กทอรี frontent/src/__tests__/ ดังนั้นให้สร้างไฟล์ frontent/src/__tests__/TodoItem.test.jsx ดังนี้
import { render, screen } from '@testing-library/react'
import { vi } from 'vitest'
import TodoItem from '../TodoItem.jsx'
describe('TodoItem', () => {
it('renders with no comments correctly', () => {
// เดี๋ยวจะเพิ่มโค้ดตรงนี้
});
});
โค้ดสำหรับเทสด้านบนยังไม่มีอะไร แต่ให้ลองเรียก
npm test
มาเพื่อดูผลลัพธ์ก่อน จะเห็นผลลัพธ์ดังนี้
> first-react-app@0.0.0 test
> vitest run
RUN v4.0.18 /home/jittat/prog/test/flask-react-todo-start/frontend
✓ src/__tests__/TodoItem.test.jsx (1 test) 4ms
✓ TodoItem (1)
✓ renders with no comments correctly 1ms
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 03:28:38
Duration 1.15s (transform 63ms, setup 167ms, import 51ms, tests 4ms, environment 682ms)
สังเกตข้อความที่พิมพ์ออกมาในส่วน TodoItem จะมีลักษณะคล้ายกับ specification (TodoItem renders with no comments correctly) ในการเขียนเทส เราจะพยายามใส่คำอธิบายแต่ละเทสในลักษณะนี้ เพื่อให้เราสามารถอ่านผลลัพธ์ได้เข้าใจง่าย
ในการทดสอบ render TodoItem เราจะต้องมี object todo ไว้เพื่อทดสอบ เราจะแก้ไฟล์ TodoItem.test.jsx ให้เป็นดังนี้
// ** ละส่วน import
const baseTodo = { // ** TodoItem พื้นฐานสำหรับทดสอบ
id: 1,
title: 'Sample Todo',
done: false,
comments: [],
};
describe('TodoItem', () => {
it('renders with no comments correctly', () => {
// *** โค้ดสำหรับเทสที่เพิ่มเข้ามา
render(
<TodoItem todo={baseTodo} />
);
expect(screen.getByText('Sample Todo')).toBeInTheDocument();
});
});
สังเกตโครงสร้างของการเขียนเทสด้านบน ดังนี้
- เราจะเรียก render
- เราใช้ฟังก์ชัน expect เพื่อตรวจสอบผลลัพธ์ สังเกตรูปแบบของชื่อฟังก์ชันต่าง ๆ ที่ออกแบบมาให้อ่านได้รู้เรื่อง เช่น toBeInTheDocument ส่วนที่เราเขียน expect นี้ จะเรียกว่า assertion ซึ่งเป็นการระบุผลลัพธ์ที่ต้องการ
โครงสร้างของการเขียนเทสแทบทั้งหมดจะมีลักษณะคล้ายกัน โดยแบ่งเป็นสามส่วนคือ (1) เตรียมการ (2) เรียกใช้สิ่งที่ต้องการเทส (3) ตรวจสอบผล
ให้ลองเรียก npm test และดูผลลัพธ์
เราจะลองแก้เทสให้ล้มเหลวเพื่อทดลองดูผลลัพธ์ ให้แก้ข้อความที่ตรวจว่ามีหรือไม่ในบรรทัด expect ให้เป็น 'Sample TodoX' แล้วทดลองรัน npm test อีกครั้ง ให้สังเกต error ที่รายงาน (จะยาวสักหน่อย)
เมื่อทดลองรันแล้ว ให้แก้เทสกลับให้เป็นแบบเดิม เพื่อให้เทสรันผ่านได้ (อย่าลืมสั่ง npm test อีกครั้ง)
TodoItem แบบที่มี comments
เราจะเพิ่ม test case ดังด้านล่าง (เติมโค้ดลงไปด้านในส่วนของฟังก์ชันที่ส่งให้ describe ให้เขียน assertion ว่ามีข้อความจาก comments เอง
it('renders with comments correctly', () => {
const todoWithComment = {
...baseTodo,
comments: [
{message: 'First comment'},
{message: 'Another comment'},
]
};
render(
<TodoItem todo={todoWithComment} />
);
expect(screen.getByText('Sample Todo')).toBeInTheDocument();
//
// *** TODO: ให้เพิ่ม assertion ว่ามีข้อความ First comment และ Another comment บนหน้าจอ
//
});
ทดลองรันด้วย npm test ให้ผ่านก่อนจะทำต่อไป