Bài 6 · Nâng cao · 24 phút· Cập nhật 11/06/2026
Unit test với pytest
Biên soạn bởi Nguyễn Anh Tuấn
Viết unit test Python với pytest: assert, fixture, parametrize, khuôn Arrange · Act · Assert - phủ ca happy/edge/error để tự tin sửa code.
Unit test không phải để “khoe code đúng”, mà để cho bạn tự tin sửa: đổi gì cũng chạy test, xanh hết là yên tâm. Xem một bug bị test bắt - bấm giữa bản LỖI và bản SỬA:
def la_so_nguyen_to(n):
# BUG: quen chan n < 2
return all(n % i for i in range(2, n)) | la_so_nguyen_to(n) | ca | mong đợi | thực tế | |
|---|---|---|---|---|
| n = 0 | biên | False | True | ✗ FAIL |
| n = 1 | biên | False | True | ✗ FAIL |
| n = 2 | thường | True | True | ✓ |
| n = 3 | thường | True | True | ✓ |
| n = 4 | thường | False | False | ✓ |
| n = 9 | thường | False | False | ✓ |
| n = 11 | thường | True | True | ✓ |
- ▸Test = lưới an toàn để SỬA code về sau mà không sợ hỏng ngầm.
- ▸Ca BIÊN (n < 2) là nơi bug hay nấp - test phủ chúng mới có giá trị.
- ▸Mỗi test theo AAA: Arrange · Act · Assert.
test_calc.py
from calc import giam_gia # ham can kiem (o calc.py)
def test_giam_gia_thuong():
gia, phan_tram = 100, 10 # Arrange
kq = giam_gia(gia, phan_tram) # Act
assert kq == 90 # Assert chạy test
uv run --with pytest pytest -q
# . [100%]
# 1 passed in 0.01s - ▸pytest tự TÌM file test_*.py & hàm test_*; assert thường + báo lỗi rõ hai vế.
- ▸Chạy: uv run --with pytest pytest -q. Thêm vào dự án: uv add --dev pytest.
- ▸-q gọn, -v chi tiết, -s cho hiện print.
test_param.py
import pytest
from calc import giam_gia
@pytest.mark.parametrize("gia, pt, kq", [
(100, 10, 90),
(200, 50, 100),
(100, 0, 100),
], ids=["thuong", "nua-gia", "khong-giam"]) # ten ro cho tung ca
def test_giam_gia(gia, pt, kq):
assert giam_gia(gia, pt) == kq
def test_so_thuc():
assert 0.1 + 0.2 == pytest.approx(0.3) # KHONG dung == truc tiep cho float Kết quả khi chạy
.... [100%] 4 passed in 0.01s
- ▸Một test chạy NHIỀU bộ dữ liệu; ids đặt tên dễ đọc cho từng ca khi báo lỗi.
- ▸float có sai số → dùng pytest.approx thay cho == để khỏi fail oan.
- ▸Thêm ca = thêm một dòng dữ liệu.
fixture chuẩn bị “nguyên liệu”; dùng yield để có cả dọn dẹp (teardown) sau test:
test_fixture.py
import pytest
@pytest.fixture # scope mac dinh = "function" (moi test mot lan)
def tai_nguyen():
r = {"con": "mo"} # setup (truoc yield)
yield r # giao cho test
r["con"] = "dong" # TEARDOWN (sau yield) - chay du test pass/fail
def test_dung(tai_nguyen):
assert tai_nguyen["con"] == "mo"
@pytest.fixture(scope="module") # tao MOT lan cho ca file
def ket_noi_nang():
... Kết quả khi chạy
. [100%] 1 passed in 0.01s
- ▸yield: trước = setup, sau = teardown (dọn dẹp), chạy kể cả khi test fail.
- ▸scope: "function" (mỗi test) / "module" / "session" - rộng hơn = tạo ít lần hơn, nhanh hơn.
- ▸Đặt fixture dùng chung vào conftest.py → mọi test trong thư mục tự dùng (khỏi import). tmp_path là fixture sẵn có.
test_chia.py
import pytest
from calc import chia
def test_happy(): assert chia(6, 2) == 3
def test_edge(): assert chia(0, 5) == 0
def test_error():
with pytest.raises(ValueError): # mong doi nem ValueError
chia(1, 0)
@pytest.mark.skip(reason="chua lam tinh nang nay")
def test_chua_lam(): ...
@pytest.mark.xfail(reason="bug da biet, chua sua")
def test_bug_da_biet(): assert chia(7, 0) == 0 Kết quả khi chạy
...sx [100%] 3 passed, 1 skipped, 1 xfailed in 0.01s
- ▸Happy (thường), Edge (biên: 0/rỗng/lớn), Error (đầu vào sai → pytest.raises).
- ▸@pytest.mark.skip: bỏ qua; @pytest.mark.xfail: cho phép fail (bug đã biết) - không làm đỏ bộ test.
- ▸Đừng chỉ test happy path - bug thật phần lớn ở edge & error.
Khi bộ test lớn, chạy đúng phần cần và đo xem code được test tới đâu:
terminal
pytest -k "giam_gia" # chi chay test co ten chua "giam_gia"
pytest -x # dung NGAY khi gap test fail dau tien
pytest --lf # chi chay lai cac test FAIL lan truoc (last-failed)
# Do do phu (coverage):
uv run --with pytest --with pytest-cov pytest --cov=calc
# ... TOTAL 42 6 86% <- 86% dong code duoc test cham - ▸-k lọc theo tên, -x dừng ở fail đầu, --lf chạy lại test vừa fail - vòng lặp sửa nhanh.
- ▸pytest-cov: đo % dòng code được test chạm; chỉ ra dòng CHƯA test.
- ▸Độ phủ cao KHÔNG bằng test tốt: phủ nhiều mà assert yếu vẫn lọt bug - phủ + assert ý nghĩa.
Tiếp theo
Câu hỏi thường gặp
Unit test kiểm MỘT đơn vị nhỏ (thường một hàm) tự động. Lợi lớn nhất là TỰ TIN SỬA về sau: đổi code rồi chạy test, xanh hết thì biết chưa làm hỏng thứ cũ. Nó cũng là tài liệu sống về cách hàm nên cư xử, kể cả ở ca biên.
Khuôn 3 bước cho mỗi test: ARRANGE - chuẩn bị đầu vào; ACT - gọi thứ cần kiểm; ASSERT - khẳng định kết quả. Tách rõ giúp test dễ đọc và mỗi test chỉ kiểm MỘT điều.
Vì float có sai số làm tròn: 0.1 + 0.2 != 0.3 (nhớ khoá cơ bản). assert 0.1+0.2 == 0.3 sẽ FAIL oan. Dùng assert 0.1+0.2 == pytest.approx(0.3) để so sánh "gần bằng" trong dung sai hợp lý - đúng cách kiểm float.
Fixture dùng yield thay vì return thì phần TRƯỚC yield là setup, phần SAU yield là TEARDOWN (dọn dẹp) - chạy kể cả khi test fail. scope quyết định fixture tạo lại bao lâu một lần: "function" (mặc định, mỗi test), "module", "session"… Scope rộng (vd session) tạo một lần, dùng lại - nhanh nhưng phải cẩn thận trạng thái dùng chung.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Lợi ích lớn nhất của unit test, theo bài học, là gì?
- 1
Đỏ rồi xanh
Dùng widget Bước 1: gõ lại
la_so_nguyen_tobản LỖI, viết test chon=0,1(thấy ĐỎ), rồi sửa cho XANH.Hoàn thành khi: Test bắt bug ca biên (đỏ); sau khi chặn
n<2thì mọi test xanh. - 2
parametrize + approx
Viết một test
parametrizecóids; thêm một ca so sánh float bằngpytest.approx.Hoàn thành khi: Nhiều ca trong một test (tên
idsrõ); ca float dùngapprox, không fail oan. - 3
fixture teardown
Viết fixture dùng
yield: tạo một tài nguyên trướcyield, dọn sauyield. In log để thấy teardown chạy sau test.Hoàn thành khi: Thứ tự: setup → test → teardown; teardown chạy kể cả khi test fail.
- 4
pytest.raises + xfail
Viết test cho
chia(1,0)raiseValueErrorbằngpytest.raises; đánh dấu một bug đã biết bằng@pytest.mark.xfail.Hoàn thành khi: Ca error pass nhờ
pytest.raises; caxfailhiện"xfailed"thay vì làm đỏ bộ test. - 5
Chạy chọn lọc & độ phủ
Chạy
pytest -k "ten_test",pytest -x, và đo coverage:uv run --with pytest --with pytest-cov pytest --cov=module.Hoàn thành khi: Bạn chạy được tập con test và xem báo cáo % độ phủ; biết dòng nào chưa được test chạm.