บทที่ 7.2: การทดสอบด้วย Pytest (การเขียน Unit Test และการใช้ Assertions)
เอกสารนี้อธิบายเกี่ยวกับแนวคิดการทดสอบซอฟต์แวร์ระดับหน่วย (Unit Testing) การใช้งาน Framework สำหรับทดสอบยอดนิยมอย่าง pytest การใช้คำสั่ง assert การจัดการสภาวะแวดล้อมด้วย Fixtures และการสอบด้วยชุดข้อมูลหลายกรณีด้วย Parametrization
1. แนวคิด Unit Testing และทำไมต้องใช้ Pytest
Unit Testing คือการเขียนโค้ดเพื่อตรวจสอบการทำงานของส่วนที่เล็กที่สุดในโปรแกรม (เช่น ฟังก์ชัน หรือ เมธอด) ว่าทำงานถูกต้องตามที่คาดหวังไว้หรือไม่เมื่อได้รับอินพุตต่างๆ
ทำไมถึงเลือกใช้ pytest แทน unittest
- Syntax เรียบง่าย: ใช้คำสั่ง
assertปกติของ Python ได้ทันที ไม่ต้องจดจำเมธอดพิเศษ เช่นassertEqual,assertTrue - Auto-Discovery: ค้นหาไฟล์และฟังก์ชันทดสอบให้อัตโนมัติ (ไฟล์ที่ขึ้นต้นด้วย
test_หรือลงท้ายด้วย_test.py) - ระบบ Fixtures: จัดการการตั้งค่าและคืนค่าสภาพแวดล้อมการทดสอบได้ง่ายและยืดหยุ่น
2. การสร้างฟังก์ชันทดสอบพื้นฐาน และการใช้ Assertions
ให้สร้างไฟล์โค้ดสำหรับทำงาน และไฟล์สำหรับทดสอบแยกกันดังนี้:
2.1 โค้ดที่ต้องการทดสอบ (calculator.py)
def add(a: float, b: float) -> float:
return a + b
def divide(a: float, b: float) -> float:
if b == 0:
raise ValueError("ไม่สามารถหารด้วยศูนย์ได้")
return a / b
2.2 ไฟล์ทดสอบ (test_calculator.py)
import pytest
from calculator import add, divide
# ฟังก์ชันทดสอบต้องขึ้นต้นด้วย test_
def test_add_positive_numbers():
assert add(2, 3) == 5
def test_add_negative_numbers():
assert add(-1, -1) == -2
assert add(-1, 1) == 0
3. การทดสอบ Exception ด้วย pytest.raises
เมื่อต้องการทดสอบว่าฟังก์ชันโยนข้อผิดพลาด (Exception) ออกมาถูกต้องตามที่ออกแบบไว้หรือไม่เมื่อรับอินพุตที่ไม่ถูกต้อง ให้ใช้ pytest.raises
def test_divide_by_zero():
# ตรวจสอบว่าต้องเกิด ValueError ขึ้นเมื่อหารด้วย 0
with pytest.raises(ValueError) as exc_info:
divide(10, 0)
# ตรวจสอบข้อความ Error Message
assert "ไม่สามารถหารด้วยศูนย์ได้" in str(exc_info.value)
4. การใช้งาน Fixtures (@pytest.fixture)
Fixture คือฟังก์ชันที่ใช้เตรียมข้อมูล สภาพแวดล้อม หรือ Resource ที่จำเป็นต้องใช้ร่วมกันในหลายๆ Test Case (เช่น การจำลองฐานข้อมูล หรือ Object ที่ต้องใช้บ่อย)
import pytest
class BankAccount:
def __init__(self, balance: float):
self.balance = balance
def deposit(self, amount: float):
self.balance += amount
def withdraw(self, amount: float):
if amount > self.balance:
raise ValueError("ยอดเงินไม่พอ")
self.balance -= amount
# สร้าง Fixture สำหรับเตรียม Account ที่มีเงิน 100 บาท
@pytest.fixture
def sample_account():
return BankAccount(100.0)
# นำ Fixture มาใช้เป็น argument ในฟังก์ชัน test
def test_initial_balance(sample_account):
assert sample_account.balance == 100.0
def test_deposit(sample_account):
sample_account.deposit(50.0)
assert sample_account.balance == 150.0
def test_withdraw_success(sample_account):
sample_account.withdraw(30.0)
assert sample_account.balance == 70.0
5. การทดสอบหลายกรณีด้วย Parametrization (@pytest.mark.parametrize)
ใช้สำหรับรันฟังก์ชันทดสอบเดิมซ้ำกันหลายๆ รอบด้วยชุดข้อมูลอินพุตและผลลัพธ์คาดหวังที่แตกต่างกัน โดยไม่ต้องเขียนฟังก์ชัน test_ แยกหลายอัน
import pytest
def is_even(number: int) -> bool:
return number % 2 == 0
# กำหนดชุดข้อมูลทดสอบ (input_val, expected_output)
@pytest.mark.parametrize("number, expected", [
(2, True),
(3, False),
(0, True),
(-2, True),
(-5, False),
])
def test_is_even(number, expected):
assert is_even(number) == expected