← Lập trình Python nâng cao

Bài 7 · Nâng cao · 24 phút· Cập nhật 11/06/2026

Integration test với pytest

Biên soạn bởi Nguyễn Anh Tuấn

Integration test trong Python: kiểm chỗ ghép DB/tệp/HTTP bằng pytest - round-trip, mock & monkeypatch, fixture và tháp kiểm thử.

Unit test kiểm một mảnh; integration test kiểm các mảnh ghép với nhau ở chỗ tiếp giáp (seam). Một bug “quên trường khi lưu” không sai trong từng hàm, nhưng round-trip lưu→đọc sẽ phát hiện. Đổi serializer giữa ĐÚNG và LỖI:

Bộ lưu (serializer):
1 · Tạo object
Task(id=1, tieu_de="Hoc Python", xong=True)

Dữ liệu gốc trong bộ nhớ.

2 · serialize → JSON
{"id":1,"tieu_de":"Hoc Python","xong":true}

Chuyển object thành str JSON để ghi ra ngoài.

3 · ghi & đọc file (tmp_path)
p.write_text(json); s = p.read_text()

Đi qua hệ thống tệp thật (thư mục tạm của test) - đây là "chỗ tiếp giáp".

4 · deserialize → object
Task(id=1, tieu_de="Hoc Python", xong=True)

Dựng lại object từ JSON đã đọc.

5 · assert loaded == goc
✓ PASS - khớp bản gốc

Round-trip toàn vẹn: lưu rồi đọc lại đúng y nguyên.

★ Unit test kiểm TỪNG mảnh; integration test kiểm CHỖ TIẾP GIÁP giữa các mảnh (ở đây: model ↔ lưu trữ ↔ tệp). Bug “quên một trường” chỉ lộ ra khi đi trọn vòng lưu → đọc lại.
  • Integration test kiểm seam: model ↔ lưu trữ ↔ tệp/CSDL/HTTP.
  • Round-trip (khứ hồi): lưu → đọc lại → phải bằng bản gốc.
  • Bug ở chỗ tiếp giáp thường lọt khỏi unit test từng mảnh.

test_roundtrip.py

from store import luu_json, tai_json    # cap luu/doc cua ban

def test_roundtrip(tmp_path):
    goc = {"id": 1, "tieu_de": "Hoc Python", "xong": True}
    f = tmp_path / "task.json"
    luu_json(goc, f)                     # serialize + ghi file
    lai = tai_json(f)                    # doc file + deserialize
    assert lai == goc                    # khu hoi phai khop

Kết quả khi chạy

.                                          [100%]
1 passed in 0.01s
  • Một test kiểm CẢ đường lưu lẫn đường đọc - bắt lệch giữa hai bên.
  • tmp_path đi qua hệ thống tệp THẬT (thư mục tạm) nhưng tự dọn.
  • Bỏ sót một trường khi lưu → lai != goc → test đỏ ngay.

Dùng SQLite thật nhưng giữ mỗi test độc lập: chạy trong một transaction rồi rollback ở teardown → DB sạch như chưa có gì:

conftest.py + test_db.py

# conftest.py
import sqlite3, pytest

@pytest.fixture(scope="session")          # tao schema MOT lan
def con():
    c = sqlite3.connect(":memory:")
    c.execute("CREATE TABLE tasks(id INTEGER, tieu_de TEXT)")
    yield c
    c.close()

@pytest.fixture                           # MOI test: transaction roi rollback
def db(con):
    con.execute("BEGIN")
    yield con
    con.rollback()                        # bo moi thay doi -> test sau thay DB sach

# test_db.py
def test_them(db):
    db.execute("INSERT INTO tasks VALUES (?, ?)", (1, "Hoc"))
    (ten,) = db.execute("SELECT tieu_de FROM tasks WHERE id=?", (1,)).fetchone()
    assert ten == "Hoc"

def test_rong(db):
    assert db.execute("SELECT COUNT(*) FROM tasks").fetchone()[0] == 0   # khong thay du lieu test truoc

Kết quả khi chạy

..                                         [100%]
2 passed in 0.02s
  • Fixture scope="session": tạo schema/kết nối MỘT lần (nhanh).
  • Fixture function: BEGIN → yield → rollback → mỗi test bắt đầu với DB SẠCH, độc lập.
  • Vẫn truy vấn THAM SỐ HOÁ (?, ...) - thói quen chống SQL injection.

Để kiểm phần ghép mà không kéo theo cả hệ thống, ta dùng test double - "đóng thế" cho thành phần thật. Ba kiểu hay gặp:

KiểuLà gìKiểm cái gì
StubTrả sẵn dữ liệu cố định (không logic)kết quả với đầu vào cho sẵn
FakeBản cài đơn giản chạy được (sqlite :memory:, dict)hành vi gần thật, nhanh
MockGhi lại lời gọiTƯƠNG TÁC: được gọi đúng tham số/số lần

mock_vs_fake.py

from unittest.mock import MagicMock

# MOCK: kiem "co goi dung khong"
gui = MagicMock()
gui("an@x.com", "hi")
gui.assert_called_once_with("an@x.com", "hi")   # kiem TUONG TAC

# FAKE: ban cai don gian, kiem KET QUA
class HopThuFake:
    def __init__(self): self.da_gui = []
    def gui(self, to, body): self.da_gui.append((to, body))
ht = HopThuFake(); ht.gui("an@x.com", "hi")
assert ht.da_gui == [("an@x.com", "hi")]
  • Stub (dữ liệu sẵn) → Fake (cài đơn giản) → Mock (kiểm lời gọi): chọn loại NHẸ nhất đủ dùng.
  • Mock kiểm TƯƠNG TÁC (assert_called_with); Fake kiểm KẾT QUẢ.
  • Với DB, ưu tiên FAKE (sqlite tạm) hơn mock toàn bộ - sát thật hơn.

Test KHÔNG nên gọi thật ra mạng (chậm, giòn, tốn phí, tác dụng phụ). Dùng monkeypatch thay client/hàm mạng bằng bản giả:

test_net.py

import gia_service                       # co ham lay_gia() goi API that

def tong_tien(so_luong):
    return so_luong * gia_service.lay_gia()

def test_tong_tien(monkeypatch):
    # Thay lay_gia bang ban gia -> KHONG goi internet
    monkeypatch.setattr(gia_service, "lay_gia", lambda: 100)
    assert tong_tien(3) == 300

def test_doc_env(monkeypatch):
    monkeypatch.setenv("API_KEY", "test-key")   # thay bien moi truong tam thoi
    ...

Kết quả khi chạy

..                                         [100%]
2 passed in 0.01s
  • monkeypatch.setattr thay hàm/đối tượng; .setenv thay biến môi trường - tự khôi phục sau test.
  • Cô lập ở RANH GIỚI (client mạng) → LOGIC của bạn vẫn chạy thật.
  • Test mạng-thật để dành cho một ít e2e có chủ đích, không phải mọi test.
Đỉnh · End-to-endRẤT ÍT - chạy cả app (vd CLI black-box); chậm & giòn nhưng giống thật nhất.
Giữa · IntegrationÍT HƠN - seam: round-trip, DB tạm, test double ở ranh giới.
Đáy · UnitNHIỀU - nhanh, rẻ, phủ logic & ca biên.
  • Nhiều unit (nhanh) → ít integration → rất ít e2e (chậm/giòn).
  • E2E CLI black-box: chạy chương trình thật, kiểm đầu vào → đầu ra.
  • Tháp cân bằng = bộ test vừa nhanh chạy vừa đủ tin cậy.

Tiếp theo

Bạn đã có bộ ba: type hints (đúng kiểu) · unit test (đúng hành vi từng mảnh) · integration test (đúng khi ghép, với test double). Bài tới: đóng gói thư viện (packaging), rồi tới phần thế giới thật - hệ thống tệp, cơ sở dữ liệu, internet - và đồng thời (GIL, async).

Câu hỏi thường gặp

Unit test kiểm MỘT mảnh cô lập (một hàm). Integration test kiểm các mảnh GHÉP với nhau ở chỗ tiếp giáp (seam): model ↔ lớp lưu trữ ↔ tệp/CSDL/HTTP. Có những bug chỉ lộ khi đi trọn đường - vd “quên một trường khi lưu” đúng trong từng hàm nhưng round-trip lưu→đọc thì khác bản gốc.

Hai cách phổ biến: (1) tạo DB MỚI mỗi test (vd sqlite :memory:) - đơn giản nhất; (2) một kết nối dùng chung (fixture scope="session") + mỗi test chạy trong một TRANSACTION rồi ROLLBACK ở teardown → DB sạch như chưa có gì, nhanh và độc lập. Cách (2) hợp khi tạo schema tốn kém.

Test double = "đóng thế" cho một thành phần thật trong test. STUB: trả sẵn dữ liệu cố định (không logic). FAKE: bản cài ĐƠN GIẢN nhưng chạy được (vd sqlite :memory: thay Postgres, dict thay cache). MOCK: ghi lại lời gọi để KIỂM nó được gọi đúng (assert đã gọi với tham số nào). Chọn loại nhẹ nhất đủ dùng.

Để THAY tạm một hàm/biến/biến môi trường trong phạm vi một test (tự khôi phục sau test). Vd monkeypatch.setattr(module, "lay_gia", lambda: 100) để khỏi gọi API thật; monkeypatch.setenv("API_KEY", "test"). Đây là cách cài fake/stub gọn nhất trong pytest.

Vì test sẽ CHẬM, GIÒN (mạng lỗi/đối tác đổi dữ liệu là đỏ oan), phụ thuộc internet, và có thể tốn phí hoặc gây tác dụng phụ thật (gửi email, tính tiền). Hãy cô lập ở RANH GIỚI: thay client mạng bằng fake/mock; còn LOGIC của bạn vẫn chạy thật.

Không. Mock quá nhiều thì test chỉ kiểm “code gọi đúng mock”, không còn kiểm hành vi thật → xanh giả. Nguyên tắc: mock ở RANH GIỚI hệ thống (mạng, dịch vụ ngoài, thời gian); với DB ưu tiên FAKE (sqlite tạm) hơn mock toàn bộ; logic của bạn để chạy thật.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Integration test tập trung kiểm cái gì khác với unit test?

  1. 1

    Bắt bug ở seam

    Dùng widget Bước 1: đổi serializer sang bản LỖI (quên "xong"); vì sao unit test cho riêng hàm lưu/đọc có thể không bắt được?

    Hoàn thành khi: Mỗi hàm "tự nó" chạy được; lỗi chỉ lộ khi GHÉP lưu→đọc và so với bản gốc.

  2. 2

    Round-trip test

    Viết test dùng tmp_path: tạo Task, luu_json rồi tai_json → khẳng định bằng bản gốc.

    Hoàn thành khi: Pass khi serializer đúng; cố tình bỏ một trường thì FAIL.

  3. 3

    DB cô lập bằng rollback

    Viết fixture: kết nối sqlite (session), mỗi test BEGIN transaction rồi ROLLBACK ở teardown. Kiểm hai test không thấy dữ liệu của nhau.

    Hoàn thành khi: Mỗi test bắt đầu với DB sạch; thêm dữ liệu ở test này không lọt sang test kia.

  4. 4

    Fake vs mock

    Có hàm gui_email(to, body) gọi dịch vụ thật. Viết test (a) dùng FAKE (lưu vào list) và (b) dùng MOCK (assert đã gọi đúng tham số).

    Hoàn thành khi: (a) kiểm KẾT QUẢ (email nằm trong list); (b) kiểm TƯƠNG TÁC (gọi 1 lần, đúng to/body). Bạn phân biệt được hai kiểu.

  5. 5

    Cô lập mạng

    lay_gia() gọi API. Dùng monkeypatch thay nó bằng hàm trả 100; test logic tong_tien dùng nó.

    Hoàn thành khi: Test KHÔNG gọi internet, chạy nhanh & ổn định; bạn kiểm được logic quanh giá trị giả.