Kỹ thuật phần mềm

Kiểm thử khi code do AI viết: tránh bài test luôn xanh

AI viết code và viết luôn bài test cho nó, nên bài test dễ chỉ xác nhận chính đoạn code vừa viết. Bài này giải thích vì sao test xanh chưa đủ, đưa một ví dụ biên 500 nghìn, và bảy cách giữ bài kiểm tra có giá trị: viết từ đặc tả, thấy đỏ trước, tự làm hỏng code, kiểm thử đột biến, kiểm thử theo thuộc tính, bảng giá trị biên và chạy thật.

  • #kiểm thử
  • #test
  • #AI
  • #kiểm thử đột biến
  • #property-based testing
  • #chất lượng

Bản trình chiếu

Bấm vào slide rồi dùng phím mũi tên để chuyển trang. Phím L đổi ngôn ngữ, phím F để toàn màn hình.

Mở toàn màn hình

Dành cho người viết hoặc duyệt code có AI tham gia, kể cả người mới. Từ kỹ thuật nào cũng có giải thích ngắn.

Ví dụ mã và các con số trong ví dụ là dựng lại để dễ hình dung. Con số "100% độ phủ, 4% điểm đột biến" lấy từ phần tóm tắt của một bài báo (mình chưa đọc toàn văn). Các công cụ được nhắc tên chỉ để làm ví dụ, mình chưa chạy chúng. Các cách làm là gợi ý của mình.


Tóm tắt nhanh#

  • Khi AI viết cả code lẫn test, test dễ chỉ xác nhận code, không xác nhận yêu cầu.
  • Test xanh chưa đủ. Độ phủ cao cũng chưa đủ: có bộ test phủ 100% mà bắt được rất ít lỗi.
  • Câu hỏi đúng không phải "test có xanh không" mà là "nếu code sai, test có đỏ không?"
  • Bảy cách: viết từ đặc tả, thấy đỏ trước, tự làm hỏng code, kiểm thử đột biến, theo thuộc tính, bảng giá trị biên, chạy thật.
  • Bắt đầu bằng thứ rẻ nhất: làm hỏng code bằng tay và xem test.

Từ khó, nói dễ#

Thuật ngữ Nói dễ hiểu
Test (bài kiểm tra) Đoạn code chạy thử code khác và so kết quả với điều mong đợi.
Độ phủ (coverage) Tỷ lệ dòng code được test chạy qua. Không nói test có kiểm tra đúng hay không.
Kiểm thử đột biến Cố ý làm hỏng code một chút rồi xem test có đỏ không.
Điểm đột biến Tỷ lệ các bản làm hỏng bị test bắt được.
Kiểm thử theo thuộc tính Thay vì vài ví dụ, mô tả một quy luật luôn đúng và để công cụ thử rất nhiều đầu vào.
Giá trị biên Giá trị nằm đúng ranh giới của quy tắc, như đúng 500 nghìn.
Test luôn xanh Test không bao giờ thất bại dù code sai.

1. Vấn đề: người viết test cũng là người viết code#

Khi một người viết code rồi viết test, người đó thường mang theo cùng một cách hiểu vào cả hai. Nếu họ hiểu sai yêu cầu, code sai và test cũng sai theo, nên test xanh. Với AI, chuyện này còn dễ xảy ra hơn: AI viết test sau khi đã có code và nhìn thấy code, nên test thường được dựng để khớp với điều code đang làm.

Đó là lý do trong danh sách 15 điểm duyệt code do AI viết có hai điểm riêng về test: test có kiểm tra điều quan trọng không, và test do chính AI viết thì phải thử làm hỏng code xem có đỏ không.

Và đây không chỉ là cảm giác. Bài báo về sinh test bằng mô hình ngôn ngữ theo hướng đột biến (MutGen, 2025) lập luận rằng độ phủ là chỉ báo yếu về khả năng phát hiện lỗi, và nêu có bộ test đạt 100% độ phủ nhưng chỉ khoảng 4% điểm đột biến. Mình đọc phần tóm tắt nên hãy coi con số này là minh họa của vấn đề, không phải thống kê chung.


2. Ví dụ: ranh giới 500 nghìn#

Ví dụ dựng lại. Yêu cầu: đơn từ 500 nghìn trở lên được giảm 10%.

AI viết:

function giamGia(tong) {
  return tong > 500000 ? tong * 0.9 : tong;
}

test("đơn lớn được giảm 10%", () => {
  expect(giamGia(600000)).toBe(540000);
});

test("đơn nhỏ không được giảm", () => {
  expect(giamGia(100000)).toBe(100000);
});

Cả hai test đều xanh, độ phủ 100%. Nhưng code sai: yêu cầu là "từ 500 nghìn trở lên", mà code dùng >, nên đơn đúng 500.000 không được giảm.

Làm hỏng thử bằng tay. Đổi > thành >=, tức là sửa code cho đúng yêu cầu. Chạy lại test: vẫn xanh. Vậy là hai bài test không phân biệt được code sai với code đúng ở ranh giới. Đây gọi là một "bản đột biến còn sống".

Thêm một bài test từ đặc tả:

test("đúng 500.000 thì được giảm", () => {
  expect(giamGia(500000)).toBe(450000);
});

Bài này đỏ với code hiện tại, và xanh sau khi sửa. Đây là bài test có giá trị: nó thất bại khi code sai.


3. Bảy cách giữ test có giá trị#

Cách 1: viết test từ đặc tả, không từ code#

Nhìn vào yêu cầu, không nhìn vào code, rồi viết ví dụ. Bài Phát triển theo đặc tả gợi ý viết trước ba đến sáu ví dụ chấp nhận được, kể cả ca lạ. Đó chính là các bài test cần có.

Nếu bắt buộc phải nhờ AI viết test, hãy cho nó đặc tả và ví dụ, đừng chỉ cho code.

Cách 2: thấy test đỏ trước#

Viết test trước khi sửa, chạy và thấy nó đỏ vì đúng lý do, rồi mới sửa cho xanh. Test mà bạn chưa từng thấy đỏ thì bạn chưa biết nó có bắt được gì không.

Tài liệu thực hành của Anthropic dùng đúng ý này khi gợi ý yêu cầu cho agent: "viết một test thất bại tái hiện lỗi, rồi sửa".

Cách 3: tự làm hỏng code#

Cách rẻ nhất, không cần công cụ. Sau khi test xanh, cố ý sửa sai code một chút rồi chạy lại:

  • Đổi > thành >=, hoặc ngược lại.
  • Đổi 0.9 thành 0.8.
  • Xóa một điều kiện, hoặc đổi && thành ||.
  • Trả về một giá trị cố định.

Nếu test vẫn xanh, bạn vừa tìm ra một chỗ test không kiểm tra. Rồi hoàn tác thay đổi.

Cách 4: kiểm thử đột biến tự động#

Làm việc ở cách 3 một cách có hệ thống: công cụ tự tạo nhiều bản làm hỏng nhỏ và báo bản nào còn sống (test không bắt được). Có công cụ cho từng ngôn ngữ, ví dụ Stryker (JavaScript), mutmut (Python), PIT (Java). Mình nhắc để bạn tìm hiểu, không phải đã chạy trong bài này.

Lưu ý: kiểm thử đột biến chậm và có thể cho nhiều "bản sống" không đáng quan tâm. Hãy dùng cho phần lõi quan trọng, không cần cho cả dự án.

Cách 5: kiểm thử theo thuộc tính#

Thay vì vài ví dụ, bạn mô tả một quy luật luôn đúng, và công cụ thử rất nhiều đầu vào ngẫu nhiên. Ví dụ với hàm giảm giá:

  • Kết quả không bao giờ lớn hơn tổng ban đầu.
  • Với mọi tổng từ 500.000 trở lên, kết quả bằng đúng 90%.
  • Với mọi tổng dưới 500.000, kết quả bằng tổng.

Công cụ như Hypothesis (Python) hay fast-check (JavaScript) làm việc này. Chúng hay tìm ra những giá trị biên mà bạn không nghĩ tới.

Cách 6: bảng giá trị biên#

Với mỗi quy tắc có ngưỡng, viết ba giá trị: ngay dưới, đúng bằng, ngay trên.

Đầu vào Mong đợi
499.999 499.999 (không giảm)
500.000 450.000 (giảm)
500.001 450.000,9 (giảm)

Cộng với các ca lạ: 0, số âm, rất lớn, sai kiểu, rỗng.

Cách 7: chạy thật một lần#

Test đơn vị không thay được việc mở ra và dùng thử. Bài Ba tiếng làm việc với một AI agent có những lỗi mà kiểm tra logic không bắt được vì chúng là điều mắt người thấy. Hãy chạy ứng dụng, bấm thử đường chính, và ghi lại.


4. Mùi của một bài test yếu#

Khi duyệt test do AI viết, tìm những dấu hiệu này:

Dấu hiệu Vì sao yếu
Không có kiểm tra (không expect hay assert) Chỉ chạy qua dòng code, không so kết quả
Kiểm tra điều hiển nhiên (kết quả không rỗng, đúng kiểu) Gần như luôn đúng
So kết quả với chính nó Luôn đúng, không phát hiện gì
Giả lập (mock) mọi thứ rồi kiểm tra rằng giả lập được gọi Chỉ kiểm tra giả lập, không kiểm tra hành vi
Chụp lại đầu ra hiện tại làm đáp án Mọi lỗi hiện có thành "đúng"
Bắt và nuốt lỗi rồi báo xanh Che mất thất bại
Chỉ thử một giá trị đẹp (như 600.000) Bỏ sót biên và ca lạ
Mong đợi tính từ cùng công thức với code Sai cùng một kiểu thì xanh cùng nhau

Mỗi bài test nên trả lời được: "Nếu code sai kiểu gì thì bài này đỏ?"


5. Danh sách kiểm sáu câu khi duyệt test do AI viết#

  1. Test được viết từ đặc tả hay từ code?
  2. Mình đã thấy nó đỏ ít nhất một lần chưa?
  3. Mình làm hỏng code một chút thì test có đỏ không? (Thử ít nhất một lần ở chỗ quan trọng.)
  4. Đã có bảng giá trị biên cho mọi ngưỡng chưa?
  5. Có kiểm tra thật (không chỉ chạy qua) và không có mock thừa?
  6. Đã chạy thật đường chính của ứng dụng chưa?

6. Khi nhờ AI viết test, nên nhờ thế nào#

Gợi ý của mình. Thay vì "viết test cho hàm này", hãy thử:

"Đây là đặc tả và ba ví dụ chấp nhận được (ví dụ ở trên). Viết test từ đặc tả, không từ code hiện có. Gồm giá trị ngay dưới, đúng bằng và ngay trên mỗi ngưỡng. Sau đó cố ý làm hỏng code theo ba cách và cho mình biết test nào đỏ. Cho mình xem lệnh đã chạy và đầu ra."

Yêu cầu cuối cùng cũng là cách để agent tự kiểm, đúng như bài Bảy sai lầm khi viết yêu cầu cho agent.


7. Nhìn cân bằng#

  • Số liệu trong bài chỉ là một mảnh. Mình chưa đọc toàn văn bài báo MutGen, và chưa có đo đạc riêng cho code do AI viết trong dự án này.
  • Kiểm thử đột biến tốn thời gian. Dùng có chọn lọc, và đừng chạy theo điểm 100%.
  • Test không phải thứ duy nhất. Duyệt code, theo dõi lỗi sau phát hành và chạy thật đều cần.
  • Test tốt cũng có chi phí bảo trì. Quá nhiều test chi tiết có thể làm việc sửa code chậm đi.
  • Ví dụ 500 nghìn là dựng lại, nhưng kiểu lỗi (sai ở biên) rất thật và phổ biến.

8. Kết luận#

Test xanh chỉ nói rằng test và code nhất trí với nhau, không nói rằng cả hai đúng. Khi AI viết cả hai, sự nhất trí đó dễ đến từ cùng một nhầm lẫn. Câu hỏi đáng hỏi là "nếu code sai, test có đỏ không?", và cách rẻ nhất để biết là làm hỏng code và xem.

Việc nhỏ làm ngay: chọn một hàm quan trọng có test do AI viết, đổi một dấu so sánh hoặc một con số trong hàm, và chạy lại test. Nếu vẫn xanh, bạn vừa tìm ra bài test đầu tiên cần viết.


Nguồn tham khảo#

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

Vì sao test do AI viết hay luôn xanh?
Vì AI thường viết test sau khi đã viết code, và test được dựng để khớp với điều code đang làm thay vì điều yêu cầu cần. Nếu code sai ở biên, bài test sinh ra từ chính code đó cũng sai theo và vẫn xanh. Ngoài ra AI hay viết kiểm tra yếu, như chỉ kiểm tra giá trị tồn tại hoặc kiểm tra điều hiển nhiên.
Độ phủ test 100% có nghĩa là code an toàn?
Không. Độ phủ chỉ đo test có chạy qua dòng code hay không, không đo test có kiểm tra kết quả đúng hay không. Bài báo về sinh test theo hướng đột biến (MutGen, 2025) nêu rằng độ phủ là chỉ báo yếu về khả năng phát hiện lỗi và có bộ test đạt 100% độ phủ nhưng chỉ khoảng 4% điểm đột biến. Mình đọc phần tóm tắt, chưa đọc toàn bài.
Kiểm thử đột biến (mutation testing) là gì?
Là cách cố ý làm hỏng code một chút (ví dụ đổi dấu lớn hơn thành lớn hơn hoặc bằng) rồi chạy lại test. Nếu test vẫn xanh, tức là test không phát hiện được thay đổi đó. Điểm đột biến là tỷ lệ các bản làm hỏng bị test bắt được.
Có cần công cụ đặc biệt không?
Không bắt buộc. Cách rẻ nhất là tự làm hỏng code bằng tay: đổi một dấu, một con số, rồi xem test có đỏ không. Khi muốn tự động hóa, có công cụ kiểm thử đột biến cho từng ngôn ngữ (ví dụ Stryker cho JavaScript, mutmut cho Python, PIT cho Java), nhưng mình chưa chạy chúng trong bài này.