Skip to content

บทที่ 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

6. คำสั่งรัน Pytest ผ่าน Command Line

# ติดตั้ง pytest
pip install pytest

# สั่งรันการทดสอบทั้งหมดในโปรเจกต์
pytest

# สั่งรันแบบแสดงรายละเอียดผลลัพธ์แต่ละ Test Case (-v: verbose)
pytest -v

# สั่งรันเฉพาะไฟล์ที่ต้องการ
pytest test_calculator.py

# สั่งรันเฉพาะฟังก์ชันที่ชื่อเข้าเงื่อนไข (-k)
pytest -k "test_add"