Đối xứng pytest

Hoàn thành

Tính năng đối trong pytest ban đầu có vẻ phức tạp, nhưng mục đích của tính năng này là đơn giản sau khi bạn hiểu được sự cố mà tính năng này giải quyết. Về cơ bản, phép đo cho phép bạn chạy cùng một hàm kiểm tra với các đầu vào khác nhau một cách hiệu quả, giúp bạn dễ dàng chạy các hiển thị chi tiết và đa dạng hơn với ít mã hơn.

Khi gọi đối, tham đối thứ nhất là một chuỗi chứa một hoặc nhiều tên đối số, ví dụ: "test\_input_". Đối số thứ hai chứa danh sách các giá trị đối số, ví dụ: ["27", "6+9", "0", "O"]. Bốn đối số cuối cùng có giá trị mặc định và là tùy chọn.

Bạn có thể tìm thấy tài liệu tham khảo VỀ API pytest tiếng Anh đối số đây: tra. Metafunc.parametrize.

Khi nào nên sử dụng phép đo từ xa

Hai kịch bản phổ biến mà bạn có thể muốn sử dụng cách đo bao gồm:

  • Khi kiểm tra vòng vòng
  • Khi nhiều xét nghiệm hiển thị cùng một hành vi

Chúng ta hãy xem lại từng ví dụ trước tiên mà không sử dụng phép đo, sau đó với ví dụ đó để hiển thị cách nó có thể cải thiện các kiểm tra của chúng tôi.

Đối với vòng lặp

Dưới đây là ví dụ về một hàm kiểm tra với một vòng for vòng lặp:

def test_string_is_digit():
    items = ["1", "10", "33"]
    for item in items:
        assert item.isdigit()

Kiểm tra này có vấn đề vì nếu không thành công, nó có thể dẫn đến một số vấn đề, bao gồm:

  • báo cáo kiểm tra không rõ ràng: Báo cáo kiểm tra không làm rõ liệu chỉ một mục có thất bại hay có nhiều lỗi hay không.
  • xem kiểm tra đơn: tất cả các mục được xem như là một kiểm tra duy nhất, làm che dấu hiệu suất mục riêng lẻ.
  • các bản sửa lỗi không chắc chắn: Nếu một lỗi được khắc phục, không có cách nào để biết liệu tất cả các sự cố có được giải quyết không mà không chạy lại toàn bộ kiểm tra hay không.

Hãy sửa đổi kiểm tra để bao gồm cụ thể hai mục dự kiến sẽ thất bại:

def test_string_is_digit():
    items = ["No", "1", "10", "33", "Yes"]
    for item in items:
        assert item.isdigit()

Chạy kiểm tra chỉ hiển thị một lỗi ngay cả khi có hai mục không hợp lệ trong danh sách đó:

$ pytest test_items.py
=================================== FAILURES ===================================
_____________________________ test_string_is_digit _____________________________
test_items.py:4: in test_string_is_digit
    assert item.isdigit()
E   AssertionError: assert False
E    +  where False = <built-in method isdigit of str object at 0x103fa1df0>()
E    +    where <built-in method isdigit of str object at 0x103fa1df0> = 'No'.isdigit
=========================== short test summary info ============================
FAILED test_items.py::test_string_is_digit - AssertionError: assert False
============================== 1 failed in 0.01s ===============================

Đây là một trường hợp sử dụng tuyệt vời đối xứng. Trước khi chúng ta có thể xem cách cập nhật bài kiểm tra, hãy khám phá một tình huống phổ biến khác không liên quan đến for vòng lặp.

Kiểm tra hiển thị hành vi tương tự

Một nhóm các bài kiểm tra thực hiện cùng một hiển thị cũng là một ứng cử viên tốt cho việc số đo. Nếu kiểm tra trước đó được viết lại với một kiểm tra cho mỗi mục, nó sẽ cho phép báo cáo lỗi tốt hơn, nhưng sẽ lặp lại:

def test_is_digit_1():
    assert "1".isdigit()

def test_is_digit_10():
    assert "10".isdigit()

def test_is_digit_33():
    assert "33".isdigit()

Các xét nghiệm này là tốt hơn trong ý nghĩa rằng một thất bại có thể dễ dàng kết hợp với một đầu vào duy nhất. Và mặc dù có vẻ bất thường khi có một số thử nghiệm tương tự, nhưng thông thường trong các bộ thử nghiệm sản xuất cố gắng có dạng hạt.

Mặc dù các bài kiểm tra sẽ tốt hơn bởi vì họ có thể báo cáo chính xác những gì không thành công (hoặc đi) họ cũng đi kèm với các vấn đề sau:

  • Mã là lặp đi lặp lại, gây ra một gánh nặng bảo trì
  • Có khả năng xảy ra lỗi chính tả và lỗi khi cập nhật kiểm tra
  • Vì chúng lặp đi lặp lại, các kỹ sư có thể không bao gồm tất cả các trường hợp sử dụng và đầu vào

Cách sử dụng phép đo từ xa

Bây giờ, bạn đã biết về một số trường hợp sử dụng đối với parametrize, hãy cập nhật kiểm tra sử dụng vòng lặp for bao gồm các mục bị lỗi.

Để sử phép đo, bạn phải nhập pytest thư viện và sau đó sử dụng làm trình trang trí trong hàm. Dưới đây là kiểm tra cập nhật:

import pytest

@pytest.mark.parametrize("item", ["No", "1", "10", "33", "Yes"])
def test_string_is_digit(item):
    assert item.isdigit()

Trước khi chạy các kiểm tra, chúng ta hãy xem qua các thay đổi.

Trình pytest.mark.parametrize() trang định nghĩa hai tham đối. Đối số thứ nhất là một chuỗi có tên là "item". Chuỗi đó được sử dụng làm đối được đặt tên cho hàm kiểm tra mà bạn thấy trong dòng tiếp theo trong định nghĩa hàm kiểm tra. Đối số thứ hai là danh sách các giá trị kiểm tra.

Báo cáo lỗi phong phú

Ở hậu trường, pytest sẽ xem mỗi mục trong danh sách đó là kiểm tra riêng. Điều đó có nghĩa là các kiểm tra vượt qua và thất bại sẽ được báo cáo riêng biệt. Hãy xem điều gì sẽ xảy ra khi chạy thử nghiệm với pytest:

$ pytest test_items.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private
collected 5 items

test_items.py F...F                                                      [100%]

=================================== FAILURES ===================================
___________________________ test_string_is_digit[No] ___________________________
test_items.py:5: in test_string_is_digit
    assert item.isdigit()
E   AssertionError: assert False
E    +  where False = <built-in method isdigit of str object at 0x102d45e30>()
E    +    where <built-in method isdigit of str object at 0x102d45e30> = 'No'.isdigit
__________________________ test_string_is_digit[Yes] ___________________________
test_items.py:5: in test_string_is_digit
    assert item.isdigit()
E   AssertionError: assert False
E    +  where False = <built-in method isdigit of str object at 0x102d45df0>()
E    +    where <built-in method isdigit of str object at 0x102d45df0> = 'Yes'.isdigit
=========================== short test summary info ============================
FAILED test_items.py::test_string_is_digit[No] - AssertionError: assert False
FAILED test_items.py::test_string_is_digit[Yes] - AssertionError: assert False
========================= 2 failed, 3 passed in 0.07s ==========================

Có một vài mục đáng chú ý trong báo cáo kiểm tra. Trước tiên, chúng ta thấy rằng từ một điểm thi duy nhất báo cáo năm bài kiểm tra tổng cộng: ba bài đạt và hai thất bại. Các lỗi được báo cáo riêng, bao gồm cả dữ liệu nhập bị lỗi.

$ pytest test_items.py
___________________________ test_string_is_digit[No] ___________________________
[...]
E    +    where <built-in method isdigit of str object at 0x102d45e30> = 'No'.isdigit
[...]
FAILED test_items.py::test_string_is_digit[No] - AssertionError: assert False

Thật khó để bỏ lỡ giá trị gây ra lỗi với rất nhiều nơi mà nó được báo cáo.

Sử dụng cờ đầu ra chi tiết

Khi các bài kiểm tra được chạy trong dòng lệnh, báo cáo kiểm tra khi kiểm tra vượt qua là tối thiểu. Dưới đây là cách kiểm tra sau khi cập nhật để khắc phục lỗi:

@pytest.mark.parametrize("item", ["0", "1", "10", "33", "9"])
def test_string_is_digit(item):
    assert item.isdigit()

Và chạy các xét nghiệm cho ra kết quả tối thiểu:

$ pytest test_items.py 
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private
collected 5 items

test_items.py .....                                                      [100%]

============================== 5 passed in 0.01s ===============================

Việc tăng mức độ chi tiết hiển thị các giá trị mà pytest chạy cho mỗi kiểm tra khi sử phép đo:

$ pytest -v test_items.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 
rootdir: /private
collected 5 items

test_items.py::test_string_is_digit[0] PASSED                            [ 20%]
test_items.py::test_string_is_digit[1] PASSED                            [ 40%]
test_items.py::test_string_is_digit[10] PASSED                           [ 60%]
test_items.py::test_string_is_digit[33] PASSED                           [ 80%]
test_items.py::test_string_is_digit[9] PASSED                            [100%]

============================== 5 passed in 0.01s ===============================

Làm thế nào để sử dụng nhiều tên đối số

Các ví dụ mà chúng ta đã thấy cho đến nay chỉ có một tên đối số trong đối số đầu tiên. Chúng tôi đã và đang sử "item" nhưng bạn có thể bao gồm nhiều tên tham đối trong chuỗi xác định đối số đầu tiên được phân tách bởi dấu phẩy.

Một trường hợp sử dụng để sử dụng nhiều tên đối số là nếu bạn muốn truyền vào một tập hợp các giá trị dự kiến để kiểm tra dựa trên giá trị đầu vào của bạn. Trong đối số thứ hai của bạn, mỗi mục trong tập hợp của bạn cần có một số lượng giá trị bằng với số lượng tên đầu vào. Ví dụ: nếu tên đầu vào của bạn là "test\_input, expected\_value", thì đối số thứ hai của bạn có thể trông giống như thế này: [("3+5", 8), ("3*4", 12)]

Kiểm tra này kiểm tra xem một đối tượng có một thuộc tính sử dụng các Python hasattr() chức năng. Nó trả về boolean tùy thuộc vào việc đối tượng có sở hữu thuộc tính liên quan hay không.

>>> hasattr(dict(), "keys")
True
>>> hasattr("string", "append")
False

hasattr() yêu cầu hai tham đối, chúng ta có thể sử phép đo theo cách sau:

@pytest.mark.parametrize("item, attribute", [("", "format"), (list(), "append")])
def test_attributes(item, attribute):
    assert hasattr(item, attribute)

Tham đối trang trí vẫn sử dụng một chuỗi đơn cho đối số thứ nhất nhưng với hai tên đối số được phân tách bằng dấu phẩy, trở thành đối số cho hàm kiểm tra. Trong trường hợp này, bạn sẽ thấy itemattribute.

Tiếp theo là danh sách hai cặp mục. Mỗi cặp trong số này đại diện cho một item và một attribute để kiểm tra.

Khi pytest không thể xây dựng một chuỗi đại diện của các đối tượng đang được truyền vào, nó tạo ra một. Bạn có thể thấy điều này khi chạy thử nghiệm:

$ pytest -v test_items.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 
rootdir: /private
collected 2 items

test_items.py::test_attributes[-format] PASSED                           [ 50%]
test_items.py::test_attributes[item1-append] PASSED                      [100%]

============================== 2 passed in 0.01s ===============================