Clean Code
10 Phần IV · Test, Hệ thống & Đồng thời Chương 9

Unit Test

Unit Tests

Test không phải "việc phụ" làm cho có. Test code quan trọng ngang production code — và một bộ test sạch chính là thứ giữ cho code sản xuất linh hoạt, dễ bảo trì, tái dùng được. Chương này gói trong vài chữ: viết test trước, giữ test sạch, mỗi test một khái niệm, và F.I.R.S.T.

01 Ba luật của TDD

Phát triển hướng-test (Test-Driven Development) xoay quanh một chu kỳ rất ngắn — chỉ khoảng 30 giây: viết một mẩu test fail, làm nó pass, lặp lại. Ba luật ràng buộc chu kỳ đó:

Luật 1 — Không production code trước test fail

Không được viết bất kỳ dòng code sản xuất nào cho tới khi đã viết một unit test đang fail.

Luật 2 — Chỉ viết test đủ để fail

Không viết test nhiều hơn mức đủ để fail — và không biên dịch được cũng tính là fail.

Luật 3 — Chỉ viết production code đủ để pass

Không viết production code nhiều hơn mức đủ để pass test đang fail hiện tại.

Test và production code được viết gần như cùng lúc, đan xen từng nhịp vài chục giây. Hệ quả: bạn tích lũy một bộ test khổng lồ bao phủ gần như mọi dòng code sản xuất — nhưng cũng chính khối lượng đó đặt ra một vấn đề quản lý.

02 Giữ cho test luôn sạch

Test code cũng quan trọng y như production code.

Robert C. Martin · Chương 9

Một quan niệm sai lầm phổ biến: test là "code hạng hai", viết cho nhanh, đầy chi tiết rối rắm cũng chẳng sao. Martin phản bác gay gắt — và đưa ra một kết luận đắt giá:

Test bẩn còn TỆ HƠN không có test. Khi production code đổi, test bẩn cực khó cập nhật theo; bug trong test ngày một nhiều, sửa test tốn hơn viết mới. Cuối cùng đội kỹ sư vứt bỏ luôn cả bộ test — và mất sạch tấm lưới an toàn.

RAG  Test cho chunker nếu viết cẩu thả sẽ thành mớ chi tiết không ai dám động vào. Test code đòi hỏi tư duy, thiết kế và chăm sóc ngang với production code:

Python · trước✗ Bẩn
def test_chunk():
    # mớ chi tiết lặp, magic number, khó đọc/khó sửa
    d = Document(id=1, text="a"*801, lang="vi", meta={"src":"x"})
    cs = Chunker(800, 0, True, False).run(d.text, d.lang, d.meta)
    assert cs[0][0:800] == "a"*800
    assert len(cs[1]) == 1 and cs[1] == "a"
Python · sau✓ Sạch
def test_chunk_splits_at_size_limit():
    chunks = chunk_text("a" * 801, size=800)   # ý định rõ ngay tên
    assert sizes_of(chunks) == [800, 1]

Vì sao: test bẩn buộc người đọc giải mã từng tham số constructor và phép cắt chuỗi — đổi Chunker một chút là test gãy hàng loạt. Test sạch giấu chi tiết sau hàm tiện ích, nên đọc một dòng là hiểu, sửa một chỗ là xong; nhờ vậy bộ test sống sót qua mọi lần đổi production.

03 Test "mở khóa" các -ilities

Vì sao test code lại quan trọng đến thế? Vì nó chính là thứ giữ cho production code đổi được mà không sợ. Không có test, mọi thay đổi đều là một canh bạc.

Linh hoạt

flexible

Có test bao phủ → bạn dám thử ý tưởng mới, vì lỗi mới lộ ra ngay lập tức.

Dễ bảo trì

maintainable

Sửa lỗi, cải thiện thiết kế mà không phá vỡ hành vi cũ — test bắt được hồi quy.

Tái dùng

reusable

Code có test là code đã được chứng minh hoạt động → an tâm tái sử dụng.

Bộ test che chắn chính là thứ xóa đi nỗi sợ thay đổi. Thiếu test, lập trình viên ngại đụng vào code cũ vì sợ tác động không lường trước — và code dần thối rữa. Nghịch lý: chính test giữ cho production code sạch chứ không phải ngược lại.

04 Test sạch · BUILD-OPERATE-CHECK & DSL

Tính dễ đọc (readability) là tất cả trong test, thậm chí còn quan trọng hơn cả ở production code. Một test sạch = rõ ràng, đơn giản, cô đọng.

Khuôn mẫu BUILD–OPERATE–CHECK

Mỗi test sạch chia làm ba phần rõ rệt, đọc như ba nhịp của một câu chuyện:

BUILD — dựng

Chuẩn bị dữ liệu & trạng thái cho tình huống test.

OPERATE — thao tác

Tác động lên đối tượng cần kiểm.

CHECK — kiểm

Khẳng định kết quả đúng như mong đợi.

Ngôn ngữ test riêng (Domain-Specific Testing Language)

RAG  Đừng để test đọc như một mớ lời gọi API lộn xộn. Hãy xây dần một tập hàm tiện ích riêng để test đọc như một câu chuyện về ý định — đúng tinh thần Martin rút từ ví dụ FitNesse (Listing 9-1 đầy chi tiết PathParser/API → Listing 9-2 dùng makePages/submitRequest/assertResponseIsXML):

Python · trước✗ Bẩn
def test_retriever_returns_relevant():
    # đầy chi tiết API gây nhiễu, lặp ở mọi test
    store = FaissStore(dim=384, metric="cosine")
    store.add(embed_batch(["paris is capital of france"]))
    q = embed_batch(["capital of france"])[0]
    res = store.search(q, k=3, nprobe=8, ef=64)
    assert res[0].id == 0 and res[0].score > 0.7
Python · sau✓ Sạch
def test_retriever_returns_relevant():
    given_documents("paris is capital of france")   # BUILD
    hits = retrieve("capital of france")             # OPERATE
    assert_top_hit_is(hits, "paris is capital of france")  # CHECK
# given_documents / retrieve / assert_top_hit_is = DSL test

Vì sao: bản bẩn phơi bày nprobe, ef, dim — chi tiết API che mất ý định; đổi cấu hình store là sửa khắp mọi test. Bản sạch dùng các hàm DSL given_documents/retrieve/assert_top_hit_is nên đọc như tiếng người, và chi tiết API chỉ nằm một chỗ trong DSL.

Tiêu chuẩn kép (dual standard): production code cần tối ưu hiệu năng, nhưng test ưu tiên sạch & dễ đọc. Ví dụ kinh điển của Martin: chuỗi "HBchL" biểu thị trạng thái 5 thiết bị phần cứng giúp test cực dễ đọc — dù getState() trả về chuỗi đó có thể không tối ưu CPU. Trong test, dễ đọc thắng.

05 Một khái niệm mỗi test

Một hướng dẫn tốt: một assert mỗi test — test nhanh đưa ra một kết luận duy nhất, dễ hiểu. Nhưng nguyên tắc quan trọng hơn là:

Single Concept per Test — mỗi test chỉ kiểm DUY NHẤT một khái niệm. Đừng viết test dài kiểm hết khái niệm này tới khái niệm khác: vừa khó đọc, vừa khó định nguyên nhân khi fail (không biết khái niệm nào hỏng).

RAG  Một test ôm đồm "kiểm luôn cả chunker" sẽ trộn nhiều khái niệm — tách thành các test một-khái-niệm:

Python · trước✗ Bẩn
def test_chunker():
    # ba khái niệm chen trong một test → fail là phải dò
    assert chunk_text("", size=800) == []            # rỗng
    cs = chunk_text("a"*801, size=800)
    assert sizes_of(cs) == [800, 1]                   # cắt theo size
    cs = chunk_text("a"*900, size=800, overlap=100)
    assert cs[1].startswith(cs[0][-100:])             # overlap
Python · sau✓ Sạch
def test_empty_text_yields_no_chunks():
    assert chunk_text("", size=800) == []

def test_splits_at_size_limit():
    assert sizes_of(chunk_text("a"*801, size=800)) == [800, 1]

def test_chunks_overlap_by_configured_amount():
    cs = chunk_text("a"*900, size=800, overlap=100)
    assert cs[1].startswith(cs[0][-100:])

Vì sao: gộp ba khái niệm (rỗng / cắt theo size / overlap) vào một test khiến khi nó fail bạn phải dò xem khái niệm nào hỏng. Tách ra, mỗi tên test nói rõ một khái niệm — fail nào lập tức chỉ thẳng nguyên nhân.

06 Năm chữ F.I.R.S.T

Test sạch tuân theo năm tính chất, gọi tắt là F.I.R.S.T — mỗi chữ một điều kiện:

F — Fast (Nhanh)

Fast

Test phải chạy nhanh. Test chậm thì bạn ngại chạy thường xuyên, nên không phát hiện lỗi sớm — và code bắt đầu thối.

I — Independent (Độc lập)

Independent

Các test không phụ thuộc nhau; test này không làm tiền đề cho test kia. Chạy theo thứ tự nào cũng được.

R — Repeatable (Lặp lại được)

Repeatable

Chạy được ở mọi môi trường — máy dev, CI, kể cả máy không có mạng. Không lệ thuộc tài nguyên ngoài bấp bênh.

S — Self-Validating (Tự kiểm)

Self-Validating

Test trả về boolean pass/fail — không bắt người đọc dò log hay so sánh kết quả bằng tay.

T — Timely (Đúng lúc)

Timely

Viết test ngay trước production code mà nó kiểm — nhờ vậy code được thiết kế để dễ test ngay từ đầu.

07 Ghi nhớ nhanh

Ba luật TDD: không production code trước test fail · chỉ viết test đủ để fail · chỉ viết production code đủ để pass.

Test bẩn tệ hơn không test. Test code quan trọng ngang production code — giữ nó sạch bằng đúng tư duy & chăm sóc đó.

Test mở khóa các -ilities: giữ production code linh hoạt, dễ bảo trì, tái dùng — và xóa nỗi sợ thay đổi.

Readability là tất cả. Dùng BUILD-OPERATE-CHECK + xây DSL test; chấp nhận tiêu chuẩn kép (test ưu tiên sạch hơn tối ưu).

Một khái niệm mỗi test (quan trọng hơn cả "một assert"). Fail thì chỉ thẳng nguyên nhân.

F.I.R.S.T: Fast · Independent · Repeatable · Self-Validating · Timely.

NguồnChương 9 (Unit Tests), Clean Code: A Handbook of Agile Software Craftsmanship — Robert C. Martin, Prentice Hall 2008.