Tại sao Unit Test thông thường vẫn để lọt lỗi?
Anh em dev mình chắc ai cũng từng trải qua cảm giác: viết code xong, unit test chạy “pass” hết, tự tin cực kỳ. Nhưng vừa deploy lên production vài tiếng là bug nổ đôm đốp. Tại sao vậy?
Vấn đề cốt lõi là chúng ta chỉ test được những gì mình “ngờ tới”. Những lỗ hổng nằm ở vùng mù thì vẫn âm thầm đợi ngày bộc phát.
Lấy ví dụ hàm cộng hai số. Thông thường, mình hay viết assert add(1, 2) == 3. Cách này gọi là Example-based Testing. Tuy nhiên, ta rất dễ bỏ quên các “edge case” cực dị: số thực quá lớn gây tràn bộ nhớ, chuỗi rỗng, hoặc ký tự lạ. Một hacker hoặc một người dùng “vui tính” nào đó hoàn toàn có thể khiến hệ thống sập nguồn bằng những input này.
Lúc này, Property-based Testing (PBT) chính là cứu cánh. Thay vì tự nặn óc nghĩ ví dụ, bạn chỉ cần định nghĩa “tính chất” của hàm. Việc còn lại là để máy tính tự động tạo ra hàng ngàn dữ liệu đầu vào quái chiêu để thử sai code của bạn.
Property-based Testing và thư viện Hypothesis
Đừng test ví dụ, hãy test tính chất
Thay vì khẳng định: “1 + 2 phải bằng 3”, bạn hãy ra lệnh: “Với bất kỳ hai số nguyên nào, kết quả phép cộng phải luôn bằng nhau bất kể thứ tự (a + b = b + a)”. Đó chính là tính chất (property). Nhiệm vụ của bạn là xác định luật chơi, còn việc tìm kẽ hở để “lách luật” là việc của công cụ.
Hypothesis: “Máy dò lỗi” nhạy bén cho Python
Trong giới Python, Hypothesis là thư viện tiêu chuẩn để triển khai PBT. Nó không tạo dữ liệu ngẫu nhiên một cách mù quáng. Hypothesis đủ nhạy để học hỏi từ những lần thất bại trước đó. Nếu tìm thấy một lỗ hổng, nó sẽ ghi nhớ và ưu tiên kiểm tra vùng dữ liệu nhạy cảm đó trong các lần chạy sau.
Bắt đầu thực chiến với Hypothesis
Cài đặt cực kỳ nhanh gọn qua pip:
pip install hypothesis pytest
Thử áp dụng cho một hàm sắp xếp danh sách quen thuộc:
# code_can_test.py
def sort_list(numbers):
return sorted(numbers)
# test_code.py
from hypothesis import given, strategies as st
from code_can_test import sort_list
@given(st.lists(st.integers()))
def test_sort_list_properties(l):
result = sort_list(l)
# 1. Độ dài danh sách không được thay đổi
assert len(result) == len(l)
# 2. Phần tử sau phải lớn hơn hoặc bằng phần tử trước
assert all(result[i] <= result[i+1] for i in range(len(result) - 1))
# 3. Không được làm mất hay thêm thắt phần tử nào so với gốc
assert sorted(result) == sorted(l)
Mặc định, decorator @given sẽ chạy test case này 100 lần với đủ loại danh sách: từ rỗng, 1 phần tử đến hàng ngàn số âm cực lớn. Nếu có bất kỳ input nào làm assert thất bại, Hypothesis sẽ chặn đứng và báo cáo ngay lập tức.
Tính năng Shrinking: Thu nhỏ lỗi để dễ debug
Đây là tính năng đáng đồng tiền bát gạo nhất. Hãy tưởng tượng Hypothesis tìm thấy một danh sách 500 số gây lỗi logic. Đọc đống đó để tìm nguyên nhân là một cực hình.
Ngay lập tức, Hypothesis sẽ tự động thực hiện “Shrinking”. Nó thử bỏ bớt phần tử, thay số lớn bằng số nhỏ (như 0 hoặc 1) để tìm ra ví dụ tối giản nhất gây lỗi. Kết quả trả về có thể chỉ ngắn gọn: “Hàm lỗi khi danh sách có đúng hai số [1, 0]”. Tiết kiệm cho bạn cả buổi chiều ngồi soi log.
Tận dụng các Strategy nâng cao
Hypothesis cung cấp các bộ tạo dữ liệu (strategies) rất phong phú, từ email, địa chỉ IP đến các object phức tạp.
Đặc biệt với Regex, việc tìm lỗi biên là cực khó nếu làm thủ công. Kinh nghiệm của mình là nên test nhanh pattern trên trình duyệt tại toolcraft.app/vi/tools/developer/regex-tester trước khi nạp vào code. Sau khi pattern chạy ổn, hãy dùng Hypothesis để “dội bom” dữ liệu nhằm đảm bảo không sót trường hợp nào.
from hypothesis import given, strategies as st
@given(st.text(alphabet=st.characters(whitelist_categories=('Lu', 'Ll')), min_size=1))
def test_process_text(s):
# Đảm bảo hàm xử lý text không bao giờ trả về None với mọi chuỗi chữ cái
result = process_text(s)
assert result is not None
Vài lưu ý nhỏ để dùng hiệu quả hơn
- Thay đổi tư duy: Đừng ép mình nghĩ về giá trị cụ thể. Hãy tập trung vào các ràng buộc và mối quan hệ giữa đầu vào – đầu ra.
- Cân đối tốc độ: Vì phải chạy nhiều lần, PBT sẽ chậm hơn unit test truyền thống. Hãy ưu tiên áp dụng cho các hàm xử lý logic nghiệp vụ quan trọng hoặc các bộ lọc dữ liệu nhạy cảm.
- Phối hợp với Pytest: Bạn chỉ cần gõ
pytest, Hypothesis sẽ tự động được kích hoạt mà không cần cấu hình rườm rà.
Lời kết
Hypothesis không sinh ra để thay thế hoàn toàn Unit Test, nhưng nó là lớp bảo vệ cực kỳ kiên cố cho code. Nó giúp bạn lôi ra ánh sáng những lỗi mà có nằm mơ chúng ta cũng không nghĩ tới.
Nếu bạn đang làm hệ thống tài chính, xử lý Big Data hoặc đơn giản là muốn ngủ ngon hơn mỗi khi deploy, hãy thử tích hợp Hypothesis ngay. Có thể lúc đầu bạn sẽ thấy khó xác định “tính chất”, nhưng khi đã quen, bạn sẽ thấy đây là khoản đầu tư cực hời cho chất lượng phần mềm.

