Ngừng đoán các trường hợp biên: Property-Based Testing với Hypothesis

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

Vượt xa Unit Testing truyền thống

Tất cả chúng ta đều đã từng viết những đoạn kiểm thử như thế này: assert add(1, 2) == 3. Nó đơn giản, gọn gàng, nhưng thường thiếu sót một cách nguy hiểm.

Kiểm thử dựa trên ví dụ (Example-based testing) hoàn toàn phụ thuộc vào khả năng dự đoán của bạn về những gì sẽ gây lỗi. Nếu bạn quên kiểm thử một chuỗi 500 ký tự, lỗi tràn số nguyên 64-bit hoặc một byte null, những lỗi đó cuối cùng sẽ xuất hiện trong log production của bạn. Ngay cả một hàm nhận hai số nguyên 32-bit cũng có hơn 18 tỷ tỷ tổ hợp đầu vào có thể xảy ra—bạn không thể viết đủ các câu lệnh assertion thủ công để bao phủ hết chúng.

Property-based testing (PBT) thay đổi hoàn toàn cách tiếp cận này. Thay vì cung cấp cho mã nguồn các giá trị cụ thể, bạn xác định các “thuộc tính” (properties) phải luôn đúng. Hypothesis là tiêu chuẩn công nghiệp cho PBT trong Python. Nó hoạt động giống như một kỹ sư QA “hỗn loạn”, ném hàng nghìn đầu vào ngẫu nhiên và đa dạng vào các hàm của bạn cho đến khi tìm ra điểm gãy.

Thiết lập môi trường

Việc cài đặt rất đơn giản vì Hypothesis tích hợp trực tiếp với pytest. Chạy lệnh sau trong terminal của bạn:

pip install hypothesis

Hypothesis không yêu cầu các tệp cấu hình phức tạp hay mã mẫu rườm rà. Nó sử dụng các Python decorators để đưa dữ liệu vào bộ kiểm thử hiện có của bạn, giúp việc áp dụng dần dần trở nên dễ dàng.

Xác định Properties và Strategies

Hypothesis dựa trên hai thành phần chính: Strategies và decorator @given. Một strategy xác định cấu trúc dữ liệu của bạn, trong khi @given hướng dẫn Hypothesis thực thi kiểm thử lặp đi lặp lại bằng dữ liệu đó.

Hãy nghĩ về một hàm đảo ngược một danh sách. Một thuộc tính cốt lõi của logic này là việc đảo ngược một danh sách hai lần sẽ trả về danh sách ban đầu. Đây là cách bạn thể hiện điều đó:

from hypothesis import given
import hypothesis.strategies as st

def reverse_list(items):
    return items[::-1]

@given(st.lists(st.integers()))
def test_reverse_twice(items):
    assert reverse_list(reverse_list(items)) == items

Trong đoạn mã này, st.lists(st.integers()) yêu cầu engine tạo ra các danh sách số nguyên. Nó sẽ thử các danh sách rỗng, danh sách có một triệu mục và danh sách chứa sys.maxsize. Theo mặc định, Hypothesis chạy kiểm thử này 100 lần với các biến thể khác nhau, bao phủ nhiều trường hợp trong vài mili giây hơn cả một con người có thể làm trong một giờ.

Các Strategies thường dùng

Thư viện bao gồm rất nhiều strategies có sẵn để mô phỏng dữ liệu thực tế:

  • st.text(): Tạo các chuỗi Unicode, bao gồm emoji, ký tự điều khiển và các chữ viết từ phải sang trái.
  • st.floats(): Tạo các số bao gồm NaN, inf, và các số subnormal thường gây lỗi logic toán học.
  • st.dictionaries(): Xây dựng các ánh xạ lồng nhau phức tạp.
  • st.emails(): Tạo các định dạng email trông có vẻ hợp lệ để kiểm thử xác thực.

Bạn cũng có thể giới hạn các strategies này. Ví dụ, st.integers(min_value=0, max_value=100) hoàn hảo để kiểm thử các trường tuổi hoặc tính toán phần trăm.

Sức mạnh của Shrinking

Gỡ lỗi một lỗi gây ra bởi một chuỗi JSON 5MB hoặc một danh sách 1.000 số thực ngẫu nhiên là một cơn ác mộng. Hypothesis khắc phục điều này thông qua một quá trình gọi là “Shrinking” (Rút gọn).

Khi Hypothesis tìm thấy một đầu vào gây ra lỗi assertion, nó không chỉ dừng lại ở đó. Nó cố gắng đơn giản hóa đầu vào thành phiên bản nhỏ nhất có thể mà vẫn gây ra lỗi. Nếu mã của bạn bị sập trên một danh sách chứa số 5.000, Hypothesis sẽ dò tìm các giá trị nhỏ hơn. Nó có thể phát hiện ra rằng lỗi thực sự được kích hoạt bởi bất kỳ số nguyên nào lớn hơn 0.

# Ví dụ về kết quả thất bại
Falsifying example: test_function(
    items=[0],  # Hypothesis đã rút gọn một danh sách phức tạp xuống còn một số không duy nhất này
)

Tính năng này biến một báo cáo lỗi mơ hồ thành một chẩn đoán chính xác. Nó cô lập trường hợp biên cho bạn một cách hiệu quả, tiết kiệm hàng giờ phân tách thủ công.

Xác thực trong quy trình CI/CD

Chạy các kiểm thử ngẫu nhiên trong môi trường CI/CD như GitHub Actions có vẻ rủi ro. Bạn có thể lo lắng về các kiểm thử “flaky” (không ổn định) — thất bại một lần rồi biến mất. Hypothesis ngăn chặn điều này bằng cách duy trì một cơ sở dữ liệu cục bộ về các ví dụ gây lỗi trong thư mục .hypothesis.

Khi một kiểm thử thất bại, Hypothesis lưu lại đầu vào cụ thể đó. Lần tới khi bạn chạy bộ kiểm thử, it sẽ kiểm tra đầu vào đã lưu đó trước. Để tận dụng tối đa điều này trong CI, bạn nên cache thư mục .hypothesis. Điều này đảm bảo rằng một khi lỗi được tìm thấy, nó sẽ luôn được phát hiện cho đến khi mã được sửa.

Tinh chỉnh quá trình tìm kiếm

Đối với logic tài chính hoặc bảo mật quan trọng, 100 ví dụ có thể là không đủ. Bạn có thể dễ dàng tăng cường độ tìm kiếm bằng cách sử dụng module settings:

from hypothesis import settings

@settings(max_examples=1000)
@given(st.integers())
def test_high_stakes_logic(n):
    ...

Mô hình Round-Trip

Một cách mạnh mẽ để sử dụng PBT là kiểm thử “Round-Trip”. Điều này lý tưởng cho bất kỳ logic tuần tự hóa (serialization) nào. Nếu bạn chuyển đổi một dictionary sang JSON và ngược lại thành một dictionary, kết quả phải khớp với bản gốc. Tôi sử dụng cách này thường xuyên để xác minh rằng các bộ mã hóa tùy chỉnh không làm mất độ chính xác hoặc làm hỏng các ký tự đặc biệt.

@given(st.dictionaries(st.text(), st.integers()))
def test_json_roundtrip(data):
    import json
    assert json.loads(json.dumps(data)) == data

Khi nào nên dùng kiểm thử truyền thống

Hypothesis rất mạnh mẽ, nhưng nó không phải là công cụ phù hợp cho mọi kịch bản. Tránh sử dụng nó cho các kiểm thử liên quan đến gọi mạng chậm, ghi dữ liệu nặng vào database hoặc các API bên ngoài. Vì engine chạy hàm của bạn hàng trăm lần, độ trễ API 500ms sẽ khiến bộ kiểm thử của bạn mất hàng phút để hoàn thành. Hãy dành PBT cho logic thuần túy, biến đổi dữ liệu và các quy tắc xác thực. Đối với kiểm thử tích hợp (integration testing), các mock chuẩn của pytest vẫn là lựa chọn tốt hơn.

Hãy chuyển đổi tư duy từ “đầu vào cụ thể này có hoạt động không?” sang “hệ thống của tôi phải luôn tuân theo những quy tắc nào?”. Bằng cách đó, bạn sẽ thấy rằng Hypothesis phát hiện ra những lỗi mà bạn chưa bao giờ tưởng tượng tới. Nó biến bộ kiểm thử của bạn từ một lưới an toàn đơn giản thành một cỗ máy săn lỗi chủ động.

Share: