Bài 17. Kiểm thử với pytest

Kiểm thử tự động giúp bạn tự tin sửa và mở rộng chương trình. Bài này dạy pytest, công cụ kiểm thử phổ biến nhất của Python: viết test, fixture, tham số hóa và đo độ phủ.

45 phútTrung cấp
Bạn sẽ học được
  • Viết test với assert trong pytest
  • Kiểm tra ngoại lệ bằng pytest.raises
  • Dùng fixture và parametrize
  • Đo độ phủ mã (coverage)
Thuật ngữ cần biết
Test case
Một tình huống kiểm tra cụ thể.
Fixture
Dữ liệu/tài nguyên chuẩn bị sẵn cho test.
Parametrize
Chạy một test với nhiều bộ dữ liệu.
Coverage
Tỉ lệ dòng mã được test chạy qua.

Khái niệm và lý thuyết

pytest tự tìm các tệp test_*.py và hàm test_*, rồi chạy. Chỉ cần câu lệnh assert thông thường; khi sai, pytest hiển thị chi tiết giá trị hai vế. Một test tốt: nhanh, độc lập, lặp lại được, kiểm tra một ý.

Nên kiểm thử các trường hợp biên: danh sách rỗng, số 0, số âm, giá trị đúng ngưỡng (điểm 5.0), dữ liệu sai định dạng.

Test đầu tiên

uv add --dev pytest pytest-cov
# src/diem.py
def xep_loai(d: float) -> str:
    if not 0 <= d <= 10:
        raise ValueError("Điểm không hợp lệ")
    if d >= 8.5: return "Giỏi"
    if d >= 7:   return "Khá"
    if d >= 5:   return "Trung bình"
    return "Yếu"

# tests/test_diem.py
import pytest
from diem import xep_loai

def test_gioi():
    assert xep_loai(9) == "Giỏi"

@pytest.mark.parametrize("d, kq", [(8.5, "Giỏi"), (7, "Khá"), (5, "Trung bình"), (4.9, "Yếu")])
def test_nguong(d, kq):
    assert xep_loai(d) == kq

def test_diem_sai():
    with pytest.raises(ValueError, match="không hợp lệ"):
        xep_loai(11)
uv run pytest -v
uv run pytest --cov=src

Fixture

@pytest.fixture
def lop_mau():
    return [{"ten": "An", "diem": 8}, {"ten": "Bình", "diem": 4}]

def test_so_dat(lop_mau):
    assert sum(sv["diem"] >= 5 for sv in lop_mau) == 1

def test_ghi_tep(tmp_path):          # fixture có sẵn: thư mục tạm
    tep = tmp_path / "a.txt"
    tep.write_text("xin chào", encoding="utf-8")
    assert tep.read_text(encoding="utf-8") == "xin chào"

Giả lập (mock)

def test_lay_nhiet_do(monkeypatch):
    class GiaPhanHoi:
        def raise_for_status(self): pass
        def json(self): return {"current": {"temperature_2m": 25}}
    monkeypatch.setattr("requests.get", lambda *a, **k: GiaPhanHoi())
    from thoi_tiet import nhiet_do
    assert nhiet_do() == 25

Mock giúp test không phụ thuộc mạng, chạy nhanh và ổn định.

Mẹo hay

Quy trình TDD: viết test trước (đỏ) → viết mã vừa đủ cho test qua (xanh) → tối ưu mã (refactor). Rất hữu ích khi làm bài tập thuật toán.

Lưu ý

Test gọi API thật hoặc phụ thuộc giờ hệ thống sẽ lúc qua lúc trượt. Dùng monkeypatch hoặc truyền thời gian vào hàm như tham số.

Thực hành
  1. Viết test cho hàm kiểm tra số nguyên tố (gồm 0, 1, 2, số âm).
  2. Viết test tham số hóa cho hàm chuyển °C sang °F.
  3. Dùng tmp_path kiểm thử hàm ghi CSV ở Bài 15.
  4. Đạt độ phủ trên 80% cho dự án Bài 10.
Tóm tắt bài học
  • pytest: tệp test_*.py, hàm test_*, dùng assert.
  • parametrize kiểm thử nhiều bộ dữ liệu.
  • fixture chuẩn bị dữ liệu; tmp_path, monkeypatch có sẵn.
  • Đo độ phủ bằng pytest-cov.