Có thể thay đổi hành vi tuyên bố khẳng định của PyTest trong Python không


18

Tôi đang sử dụng Python khẳng định các câu lệnh để khớp với hành vi thực tế và dự kiến. Tôi không có quyền kiểm soát đối với những trường hợp này nếu có trường hợp kiểm tra lỗi hủy bỏ. Tôi muốn kiểm soát lỗi xác nhận và muốn xác định xem tôi có muốn hủy bỏ testcase khi xác nhận thất bại hay không.

Ngoài ra tôi muốn thêm một cái gì đó như nếu có lỗi xác nhận thì trường hợp kiểm tra nên được tạm dừng và người dùng có thể tiếp tục bất cứ lúc nào.

Tôi không biết làm thế nào để làm điều này

Mã ví dụ, chúng tôi đang sử dụng pytest ở đây

import pytest
def test_abc():
    a = 10
    assert a == 10, "some error message"

Below is my expectation

Khi khẳng định ném một khẳng địnhError, tôi nên có một tùy chọn tạm dừng testcase và có thể gỡ lỗi và tiếp tục lại sau đó. Để tạm dừng và tiếp tục tôi sẽ sử dụng tkintermô-đun. Tôi sẽ thực hiện một chức năng khẳng định như dưới đây

import tkinter
import tkinter.messagebox

top = tkinter.Tk()

def _assertCustom(assert_statement, pause_on_fail = 0):
    #assert_statement will be something like: assert a == 10, "Some error"
    #pause_on_fail will be derived from global file where I can change it on runtime
    if pause_on_fail == 1:
        try:
            eval(assert_statement)
        except AssertionError as e:
            tkinter.messagebox.showinfo(e)
            eval (assert_statement)
            #Above is to raise the assertion error again to fail the testcase
    else:
        eval (assert_statement)

Đi về phía trước tôi phải thay đổi mọi tuyên bố khẳng định với chức năng này là

import pytest
def test_abc():
    a = 10
    # Suppose some code and below is the assert statement 
    _assertCustom("assert a == 10, 'error message'")

Đây là quá nhiều nỗ lực đối với tôi khi tôi phải thay đổi ở hàng ngàn nơi mà tôi đã sử dụng khẳng định. Có cách nào dễ dàng để làm điều đó trongpytest

Summary:Tôi cần một cái gì đó để tôi có thể tạm dừng testcase khi thất bại và sau đó tiếp tục lại sau khi gỡ lỗi. Tôi biết tkintervà đó là lý do tôi đã sử dụng nó. Bất kỳ ý tưởng nào khác sẽ được chào đón

Note: Mã trên chưa được thử nghiệm. Có thể có lỗi cú pháp nhỏ quá

Chỉnh sửa: Cảm ơn câu trả lời. Mở rộng câu hỏi này một chút về phía trước bây giờ. Nếu tôi muốn thay đổi hành vi khẳng định. Hiện tại khi có một testcase lỗi xác nhận thoát. Điều gì sẽ xảy ra nếu tôi muốn chọn nếu tôi cần thoát testcase khi xác nhận thất bại cụ thể hay không. Tôi không muốn viết chức năng xác nhận tùy chỉnh như đã đề cập ở trên vì cách này tôi phải thay đổi ở số lượng địa điểm


3
Bạn có thể cho chúng tôi một ví dụ mã về những gì bạn muốn làm không?
mrblewog

1
Đừng sử dụng assertmà hãy viết các chức năng kiểm tra của riêng bạn để làm những gì bạn muốn.
molbdnilo

Tại sao bạn không chèn khẳng định trong khối thử và thông báo lỗi ngoại trừ ?
Prathik Kini

1
Có vẻ như những gì bạn thực sự muốn là sử dụng pytestcho các trường hợp thử nghiệm của bạn. Nó hỗ trợ sử dụng các bài kiểm tra khẳng địnhbỏ qua cùng với nhiều tính năng khác giúp cho việc viết các bộ kiểm tra dễ dàng hơn.
blubberdiblub

1
Sẽ không đơn giản để viết một công cụ đơn giản có thể thay thế mọi thứ assert cond, "msg"trong mã của bạn bằng _assertCustom("assert cond, 'msg'")? Có lẽ một sedlót có thể làm điều đó.
NPE

Câu trả lời:


23

Bạn đang sử dụng pytest, cung cấp cho bạn nhiều tùy chọn để tương tác với các bài kiểm tra thất bại. Nó cung cấp cho bạn các tùy chọn dòng lệnh và một số hook để thực hiện điều này. Tôi sẽ giải thích cách sử dụng từng và nơi bạn có thể thực hiện các tùy chỉnh để phù hợp với nhu cầu gỡ lỗi cụ thể của mình.

Tôi cũng sẽ đi vào các tùy chọn kỳ lạ hơn cho phép bạn bỏ qua các xác nhận cụ thể hoàn toàn, nếu bạn thực sự cảm thấy bạn phải.

Xử lý các trường hợp ngoại lệ, không khẳng định

Lưu ý rằng một bài kiểm tra thất bại thường không dừng pytest; chỉ khi bạn kích hoạt một cách rõ ràng yêu cầu nó thoát ra sau một số lần thất bại nhất định . Ngoài ra, các bài kiểm tra thất bại vì một ngoại lệ được nêu ra; asserttăng AssertionErrornhưng đó không phải là ngoại lệ duy nhất sẽ khiến bài kiểm tra thất bại! Bạn muốn kiểm soát cách xử lý ngoại lệ, không thay đổi assert.

Tuy nhiên, một khẳng định không thành công sẽ kết thúc bài kiểm tra cá nhân. Đó là bởi vì một khi một ngoại lệ được đưa ra bên ngoài một try...exceptkhối, Python sẽ loại bỏ khung chức năng hiện tại và không có gì phải quay lại.

Tôi không nghĩ rằng đó là những gì bạn muốn, đánh giá bằng mô tả của bạn về _assertCustom()những nỗ lực của bạn để chạy lại khẳng định, nhưng dù sao tôi cũng sẽ thảo luận về các lựa chọn của bạn.

Gỡ lỗi sau khi chết trong pytest với pdb

Đối với các tùy chọn khác nhau để xử lý các lỗi trong trình gỡ lỗi, tôi sẽ bắt đầu với --pdbcông tắc dòng lệnh , mở ra lời nhắc gỡ lỗi tiêu chuẩn khi thử nghiệm thất bại (đầu ra bị lỗi vì ngắn gọn):

$ mkdir demo
$ touch demo/__init__.py
$ cat << EOF > demo/test_foo.py
> def test_ham():
>     assert 42 == 17
> def test_spam():
>     int("Vikings")
> EOF
$ pytest demo/test_foo.py --pdb
[ ... ]
test_foo.py:2: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../demo/test_foo.py(2)test_ham()
-> assert 42 == 17
(Pdb) q
Exit: Quitting debugger
[ ... ]

Với công tắc này, khi thử nghiệm thất bại, pytest bắt đầu phiên gỡ lỗi sau khi chết . Đây thực chất là chính xác những gì bạn muốn; để dừng mã tại điểm kiểm tra thất bại và mở trình gỡ lỗi để xem trạng thái kiểm tra của bạn. Bạn có thể tương tác với các biến cục bộ của bài kiểm tra, toàn cục và địa phương và toàn cầu của mọi khung trong ngăn xếp.

Ở đây pytest cung cấp cho bạn toàn quyền kiểm soát xem có thoát hay không sau thời điểm này: nếu bạn sử dụng qlệnh thoát thì pytest cũng thoát khỏi quá trình chạy, sử dụng cđể tiếp tục sẽ trả lại quyền kiểm soát cho pytest và thử nghiệm tiếp theo được thực hiện.

Sử dụng một trình gỡ lỗi thay thế

Bạn không bị ràng buộc với pdbtrình gỡ lỗi cho điều này; bạn có thể thiết lập một trình gỡ lỗi khác với công --pdbclstắc. Bất kỳ triển khai pdb.Pdb()tương thích nào cũng sẽ hoạt động, bao gồm triển khai trình gỡ lỗi IPython hoặc hầu hết các trình gỡ lỗi Python khác ( trình gỡ lỗi pudb yêu cầu -schuyển đổi được sử dụng hoặc một plugin đặc biệt ). Công tắc có một mô-đun và lớp, ví dụ để sử dụng, pudbbạn có thể sử dụng:

$ pytest -s --pdb --pdbcls=pudb.debugger:Debugger

Bạn có thể sử dụng tính năng này để viết lớp wrapper của riêng bạn xung quanh Pdbmà chỉ đơn giản trả về ngay lập tức nếu sự thất bại cụ thể không phải là một cái gì đó bạn đang quan tâm. pytestSử dụng Pdb()giống hệt như pdb.post_mortem()thực hiện :

p = Pdb()
p.reset()
p.interaction(None, t)

Ở đây, tlà một đối tượng truy nguyên . Khi p.interaction(None, t)trả về, pytesttiếp tục với thử nghiệm tiếp theo, trừ khi p.quitting được đặt thành True(tại điểm pytest sau đó thoát ra).

Dưới đây là một ví dụ triển khai in ra rằng chúng tôi đang từ chối gỡ lỗi và trả về ngay lập tức, trừ khi thử nghiệm được nêu ra ValueError, được lưu dưới dạng demo/custom_pdb.py:

import pdb, sys

class CustomPdb(pdb.Pdb):
    def interaction(self, frame, traceback):
        if sys.last_type is not None and not issubclass(sys.last_type, ValueError):
            print("Sorry, not interested in this failure")
            return
        return super().interaction(frame, traceback)

Khi tôi sử dụng điều này với bản demo ở trên, đây là đầu ra (một lần nữa, được giải thích cho ngắn gọn):

$ pytest test_foo.py -s --pdb --pdbcls=demo.custom_pdb:CustomPdb
[ ... ]
    def test_ham():
>       assert 42 == 17
E       assert 42 == 17

test_foo.py:2: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Sorry, not interested in this failure
F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    def test_spam():
>       int("Vikings")
E       ValueError: invalid literal for int() with base 10: 'Vikings'

test_foo.py:4: ValueError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../test_foo.py(4)test_spam()
-> int("Vikings")
(Pdb)

Các nội tâm trên sys.last_typeđể xác định xem thất bại có 'thú vị' hay không.

Tuy nhiên, tôi thực sự không thể đề xuất tùy chọn này trừ khi bạn muốn viết trình gỡ lỗi của riêng mình bằng tkInter hoặc một cái gì đó tương tự. Lưu ý rằng đó là một công việc lớn.

Lỗi lọc; chọn và chọn thời điểm mở trình gỡ lỗi

Cấp độ tiếp theo lên là pytest gỡ lỗi và tương tác móc ; đây là những điểm móc cho các tùy chỉnh hành vi, để thay thế hoặc nâng cao cách thức pytest thường xử lý những việc như xử lý ngoại lệ hoặc nhập trình gỡ lỗi thông qua pdb.set_trace()hoặc breakpoint()(Python 3.7 hoặc mới hơn).

Việc triển khai bên trong của hook này cũng chịu trách nhiệm in >>> entering PDB >>>banner ở trên, do đó, sử dụng hook này để ngăn trình gỡ lỗi chạy có nghĩa là bạn sẽ không thấy đầu ra này. Bạn có thể có hook riêng của mình sau đó ủy quyền cho hook ban đầu khi lỗi kiểm tra là 'thú vị', và do đó, lỗi kiểm tra bộ lọc không phụ thuộc vào trình gỡ lỗi bạn đang sử dụng! Bạn có thể truy cập vào việc thực hiện nội bộ bằng cách truy cập nó theo tên ; plugin hook nội bộ cho cái này được đặt tên pdbinvoke. Để ngăn nó chạy, bạn cần hủy đăng ký nhưng lưu tham chiếu để chúng tôi có thể gọi trực tiếp khi cần.

Đây là một mẫu thực hiện của một cái móc như vậy; bạn có thể đặt cái này vào bất kỳ plugin nào trong số các vị trí được tải từ ; Tôi đặt nó vào demo/conftest.py:

import pytest

@pytest.hookimpl(trylast=True)
def pytest_configure(config):
    # unregister returns the unregistered plugin
    pdbinvoke = config.pluginmanager.unregister(name="pdbinvoke")
    if pdbinvoke is None:
        # no --pdb switch used, no debugging requested
        return
    # get the terminalreporter too, to write to the console
    tr = config.pluginmanager.getplugin("terminalreporter")
    # create or own plugin
    plugin = ExceptionFilter(pdbinvoke, tr)

    # register our plugin, pytest will then start calling our plugin hooks
    config.pluginmanager.register(plugin, "exception_filter")

class ExceptionFilter:
    def __init__(self, pdbinvoke, terminalreporter):
        # provide the same functionality as pdbinvoke
        self.pytest_internalerror = pdbinvoke.pytest_internalerror
        self.orig_exception_interact = pdbinvoke.pytest_exception_interact
        self.tr = terminalreporter

    def pytest_exception_interact(self, node, call, report):
        if not call.excinfo. errisinstance(ValueError):
            self.tr.write_line("Sorry, not interested!")
            return
        return self.orig_exception_interact(node, call, report)

Các plugin trên sử dụng các TerminalReporterplugin nội bộ để viết ra các dòng đến thiết bị đầu cuối; điều này làm cho đầu ra sạch hơn khi sử dụng định dạng trạng thái kiểm tra nhỏ gọn mặc định và cho phép bạn ghi mọi thứ vào thiết bị đầu cuối ngay cả khi bật chức năng chụp đầu ra.

Ví dụ đăng ký đối tượng plugin bằng pytest_exception_interacthook thông qua hook khác pytest_configure(), nhưng đảm bảo rằng nó chạy đủ muộn (sử dụng @pytest.hookimpl(trylast=True)) để có thể hủy đăng ký pdbinvokeplugin nội bộ . Khi hook được gọi, ví dụ kiểm tra call.exceptinfođối tượng ; bạn cũng có thể kiểm tra nút hoặc báo cáo .

Với mã mẫu ở trên demo/conftest.py, test_hamlỗi thử nghiệm bị bỏ qua, chỉ có test_spamlỗi thử nghiệm, tăng lên ValueError, dẫn đến mở nhắc gỡ lỗi:

$ pytest demo/test_foo.py --pdb
[ ... ]
demo/test_foo.py F
Sorry, not interested!

demo/test_foo.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    def test_spam():
>       int("Vikings")
E       ValueError: invalid literal for int() with base 10: 'Vikings'

demo/test_foo.py:4: ValueError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../demo/test_foo.py(4)test_spam()
-> int("Vikings")
(Pdb) 

Để lặp lại, cách tiếp cận trên có một ưu điểm nữa là bạn có thể kết hợp điều này với bất kỳ trình gỡ lỗi nào hoạt động với pytest , bao gồm pudb hoặc trình gỡ lỗi IPython:

$ pytest demo/test_foo.py --pdb --pdbcls=IPython.core.debugger:Pdb
[ ... ]
demo/test_foo.py F
Sorry, not interested!

demo/test_foo.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    def test_spam():
>       int("Vikings")
E       ValueError: invalid literal for int() with base 10: 'Vikings'

demo/test_foo.py:4: ValueError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../demo/test_foo.py(4)test_spam()
      1 def test_ham():
      2     assert 42 == 17
      3 def test_spam():
----> 4     int("Vikings")

ipdb>

Nó cũng có nhiều bối cảnh hơn về những gì thử nghiệm đã được chạy (thông qua nodeđối số) và truy cập trực tiếp vào ngoại lệ được nêu ra (thông qua call.excinfo ExceptionInfothể hiện).

Lưu ý rằng các plugin gỡ lỗi pytest cụ thể (như pytest-pudbhoặc pytest-pycharm) đăng ký pytest_exception_interacthooksp riêng của chúng. Việc triển khai đầy đủ hơn sẽ phải lặp qua tất cả các plugin trong trình quản lý plugin để tự động ghi đè các plugin tùy ý, sử dụng config.pluginmanager.list_name_pluginhasattr()kiểm tra từng plugin.

Làm cho thất bại biến mất hoàn toàn

Mặc dù điều này cho phép bạn kiểm soát hoàn toàn việc gỡ lỗi thử nghiệm thất bại, nhưng điều này vẫn khiến thử nghiệm thất bại ngay cả khi bạn chọn không mở trình gỡ lỗi cho một thử nghiệm nhất định. Nếu bạn muốn làm cho thất bại biến mất hoàn toàn, bạn có thể sử dụng một cái móc khác : pytest_runtest_call().

Khi pytest chạy thử nghiệm, nó sẽ chạy thử nghiệm thông qua hook ở trên, dự kiến ​​sẽ trả về Nonehoặc đưa ra một ngoại lệ. Từ đây, một báo cáo được tạo, tùy ý một mục nhật ký được tạo và nếu thử nghiệm thất bại, pytest_exception_interact()hook đã nói ở trên được gọi. Vì vậy, tất cả những gì bạn cần làm là thay đổi kết quả mà cái móc này tạo ra; thay vì một ngoại lệ, nó sẽ không trả lại bất cứ thứ gì cả.

Cách tốt nhất để làm điều đó là sử dụng một cái bọc hook . Các hàm bao móc không phải thực hiện công việc thực tế, nhưng thay vào đó được tạo cơ hội để thay đổi những gì xảy ra với kết quả của một móc. Tất cả bạn phải làm là thêm dòng:

outcome = yield

trong triển khai trình bao móc của bạn và bạn có quyền truy cập vào kết quả hook , bao gồm cả ngoại lệ kiểm tra thông qua outcome.excinfo. Thuộc tính này được đặt thành một bộ (loại, ví dụ, truy nguyên) nếu một ngoại lệ được đưa ra trong thử nghiệm. Ngoài ra, bạn có thể gọi outcome.get_result()và sử dụng try...exceptxử lý tiêu chuẩn .

Vì vậy, làm thế nào để bạn thực hiện một bài kiểm tra thất bại? Bạn có 3 tùy chọn cơ bản:

  • Bạn có thể đánh dấu bài kiểm tra là một thất bại dự kiến , bằng cách gọi pytest.xfail()trong trình bao bọc.
  • Bạn có thể đánh dấu mục bị bỏ qua , điều này giả vờ rằng bài kiểm tra không bao giờ được chạy ở nơi đầu tiên, bằng cách gọi pytest.skip().
  • Bạn có thể loại bỏ ngoại lệ, bằng cách sử dụng outcome.force_result()phương pháp ; đặt kết quả thành một danh sách trống ở đây (có nghĩa là: hook đã đăng ký không tạo ra gì ngoài None) và ngoại lệ được xóa hoàn toàn.

Những gì bạn sử dụng là tùy thuộc vào bạn. Trước tiên, hãy đảm bảo kiểm tra kết quả cho các bài kiểm tra bị bỏ qua và dự kiến ​​thất bại vì bạn không cần phải xử lý các trường hợp đó như thể bài kiểm tra thất bại. Bạn có thể truy cập các ngoại lệ đặc biệt mà các tùy chọn này đưa ra thông qua pytest.skip.Exceptionpytest.xfail.Exception.

Dưới đây là một triển khai ví dụ đánh dấu các bài kiểm tra thất bại không nêu ra ValueError, như đã bỏ qua :

import pytest

@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
    outcome = yield
    try:
        outcome.get_result()
    except (pytest.xfail.Exception, pytest.skip.Exception, pytest.exit.Exception):
        raise  # already xfailed,  skipped or explicit exit
    except ValueError:
        raise  # not ignoring
    except (pytest.fail.Exception, Exception):
        # turn everything else into a skip
        pytest.skip("[NOTRUN] ignoring everything but ValueError")

Khi đưa vào conftest.pyđầu ra trở thành:

$ pytest -r a demo/test_foo.py
============================= test session starts =============================
platform darwin -- Python 3.8.0, pytest-3.10.0, py-1.7.0, pluggy-0.8.0
rootdir: ..., inifile:
collected 2 items

demo/test_foo.py sF                                                      [100%]

=================================== FAILURES ===================================
__________________________________ test_spam ___________________________________

    def test_spam():
>       int("Vikings")
E       ValueError: invalid literal for int() with base 10: 'Vikings'

demo/test_foo.py:4: ValueError
=========================== short test summary info ============================
FAIL demo/test_foo.py::test_spam
SKIP [1] .../demo/conftest.py:12: [NOTRUN] ignoring everything but ValueError
===================== 1 failed, 1 skipped in 0.07 seconds ======================

Tôi đã sử dụng -r acờ để làm cho nó rõ ràng hơn test_hamđã bị bỏ qua.

Nếu bạn thay thế pytest.skip()cuộc gọi bằng pytest.xfail("[XFAIL] ignoring everything but ValueError"), thử nghiệm được đánh dấu là một lỗi dự kiến:

[ ... ]
XFAIL demo/test_foo.py::test_ham
  reason: [XFAIL] ignoring everything but ValueError
[ ... ]

và sử dụng outcome.force_result([])đánh dấu nó như đã thông qua:

$ pytest -v demo/test_foo.py  # verbose to see individual PASSED entries
[ ... ]
demo/test_foo.py::test_ham PASSED                                        [ 50%]

Tùy thuộc vào bạn, cái nào bạn cảm thấy phù hợp nhất với trường hợp sử dụng của bạn. Cho skip()xfail()tôi bắt chước định dạng tin nhắn tiêu chuẩn (có tiền tố là [NOTRUN]hoặc [XFAIL]) nhưng bạn có thể tự do sử dụng bất kỳ định dạng tin nhắn nào bạn muốn.

Trong cả ba trường hợp, pytest sẽ không mở trình gỡ lỗi cho các thử nghiệm mà kết quả bạn đã thay đổi bằng phương pháp này.

Thay đổi tuyên bố khẳng định cá nhân

Nếu bạn muốn thay đổi assertcác bài kiểm tra trong một bài kiểm tra , thì bạn đang thiết lập cho mình nhiều công việc hơn. Vâng, điều này là có thể về mặt kỹ thuật , nhưng chỉ bằng cách viết lại chính đoạn mã mà Python sẽ thực thi vào thời gian biên dịch .

Khi bạn sử dụng pytest, điều này thực sự đã được thực hiện . Pytest viết lại các assertcâu lệnh để cung cấp cho bạn nhiều ngữ cảnh hơn khi các xác nhận của bạn thất bại ; xem bài đăng trên blog này để biết tổng quan tốt về chính xác những gì đang được thực hiện, cũng như _pytest/assertion/rewrite.pymã nguồn . Lưu ý rằng mô-đun đó dài hơn 1k dòng và yêu cầu bạn hiểu cách cây cú pháp trừu tượng của Python hoạt động. Nếu bạn làm như vậy, bạn có thể điều chỉnh mô-đun đó để thêm các sửa đổi của riêng bạn ở đó, bao gồm cả xung quanh assertvới một try...except AssertionError:trình xử lý.

Tuy nhiên , bạn không thể vô hiệu hóa hoặc bỏ qua các xác nhận có chọn lọc, bởi vì các câu lệnh tiếp theo có thể dễ dàng phụ thuộc vào trạng thái (sắp xếp đối tượng cụ thể, các biến được đặt, v.v.) mà một xác nhận bị bỏ qua có nghĩa là để bảo vệ chống lại. Nếu một thử nghiệm khẳng định foolà không None, thì sau đó khẳng định sẽ dựa vào foo.barsự tồn tại, sau đó bạn chỉ cần chạy vào AttributeErrorđó, v.v. Hãy kiên trì nâng cao ngoại lệ, nếu bạn cần đi theo con đường này.

Tôi sẽ không đi sâu vào chi tiết hơn về việc viết lại assertsở đây, vì tôi không nghĩ rằng việc này đáng để theo đuổi, không được đưa ra số lượng công việc liên quan và với việc gỡ lỗi sau khi cho phép bạn truy cập vào trạng thái của bài kiểm tra tại điểm khẳng định thất bại nào .

Lưu ý rằng nếu bạn muốn làm điều này, bạn không cần sử dụng eval()(dù sao nó cũng không hoạt động, assertlà một tuyên bố, vì vậy bạn cần phải sử dụng exec()thay thế), bạn cũng không phải chạy xác nhận hai lần (mà có thể dẫn đến các vấn đề nếu biểu thức được sử dụng trong trạng thái xác nhận thay đổi). Thay vào đó, bạn sẽ nhúng ast.Assertnút bên trong một ast.Trynút và đính kèm một trình xử lý ngoại trừ sử dụng ast.Raisenút trống để nêu lại ngoại lệ đã bị bắt.

Sử dụng trình gỡ lỗi để bỏ qua các câu lệnh khẳng định.

Trình gỡ lỗi Python thực sự cho phép bạn bỏ qua các câu lệnh , sử dụng lệnh j/jump . Nếu bạn biết trước rằng một xác nhận cụ thể sẽ thất bại, bạn có thể sử dụng điều này để bỏ qua nó. Bạn có thể chạy thử nghiệm của mình --trace, mở trình gỡ lỗi vào đầu mỗi thử nghiệm , sau đó đưa ra một thử nghiệmj <line after assert> để bỏ qua khi trình gỡ lỗi bị tạm dừng ngay trước khi xác nhận.

Bạn thậm chí có thể tự động hóa điều này. Sử dụng các kỹ thuật trên, bạn có thể xây dựng một trình gỡ lỗi tùy chỉnh

  • sử dụng pytest_testrun_call()móc để bắt AssertionErrorngoại lệ
  • trích xuất số dòng 'vi phạm' từ truy nguyên và có lẽ với một số phân tích mã nguồn xác định số dòng trước và sau khi xác nhận cần thiết để thực hiện bước nhảy thành công
  • chạy thử nghiệm một lần nữa , nhưng lần này sử dụng một Pdblớp con đặt điểm dừng trên dòng trước khi xác nhận và tự động thực hiện bước nhảy sang giây khi điểm dừng được nhấn, tiếp theo là ctiếp tục.

Hoặc, thay vì chờ xác nhận thất bại, bạn có thể tự động hóa thiết lập các điểm dừng cho từng điểm assertđược tìm thấy trong một thử nghiệm (một lần nữa sử dụng phân tích mã nguồn, bạn có thể trích xuất một cách tầm thường các số dòng cho ast.Assertcác nút trong AST của thử nghiệm), thực hiện thử nghiệm được xác nhận sử dụng các lệnh gỡ lỗi theo kịch bản và sử dụng jumplệnh để bỏ qua chính xác nhận đó. Bạn sẽ phải đánh đổi; chạy tất cả các thử nghiệm theo trình gỡ lỗi (chậm vì trình thông dịch phải gọi hàm theo dõi cho mỗi câu lệnh) hoặc chỉ áp dụng điều này cho các thử nghiệm thất bại và trả giá khi chạy lại các thử nghiệm đó từ đầu.

Một plugin như vậy sẽ có rất nhiều công việc để tạo, tôi sẽ không viết một ví dụ ở đây, một phần vì dù sao nó cũng không phù hợp với câu trả lời, và một phần vì tôi không nghĩ rằng nó đáng để dành thời gian . Tôi chỉ cần mở trình gỡ lỗi và thực hiện bước nhảy thủ công. Một xác nhận thất bại chỉ ra một lỗi trong chính bài kiểm tra hoặc bài kiểm tra mã, vì vậy bạn cũng có thể chỉ tập trung vào việc gỡ lỗi vấn đề.


7

Bạn có thể đạt được chính xác những gì bạn muốn mà không cần sửa đổi mã với pytest --pdb .

Với ví dụ của bạn:

import pytest
def test_abc():
    a = 9
    assert a == 10, "some error message"

Chạy với --pdb:

py.test --pdb
collected 1 item

test_abc.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    def test_abc():
        a = 9
>       assert a == 10, "some error message"
E       AssertionError: some error message
E       assert 9 == 10

test_abc.py:4: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /private/tmp/a/test_abc.py(4)test_abc()
-> assert a == 10, "some error message"
(Pdb) p a
9
(Pdb)

Ngay sau khi thử nghiệm thất bại, bạn có thể gỡ lỗi nó bằng trình gỡ lỗi python dựng sẵn. Nếu bạn đã gỡ lỗi xong, bạn có thể làm continuevới các bài kiểm tra còn lại.


Điều này sẽ dừng lại khi trường hợp thử nghiệm thất bại hoặc bước thử nghiệm thất bại.
Nitesh

Vui lòng kiểm tra các tài liệu được liên kết: doc.pytest.org/en/latest/ từ
gnvk

Ý tưởng tuyệt vời Nhưng nếu sử dụng --pdb, testcase sẽ tạm dừng ở mọi thất bại. Tôi có thể quyết định trong thời gian chạy mà tôi muốn tạm dừng trường hợp thử nghiệm không
Nitesh

5

Nếu bạn đang sử dụng PyCharm thì bạn có thể thêm Điểm dừng ngoại lệ để tạm dừng thực thi bất cứ khi nào xác nhận thất bại. Chọn Xem điểm dừng (CTRL-SHIFT-F8) và thêm trình xử lý ngoại lệ nâng cao cho AssertsError. Lưu ý rằng điều này có thể làm chậm việc thực hiện các bài kiểm tra.

Mặt khác, nếu bạn không ngại tạm dừng vào cuối mỗi bài kiểm tra thất bại (ngay trước khi nó xảy ra lỗi) thay vì tại thời điểm xác nhận thất bại, thì bạn có một vài lựa chọn. Tuy nhiên, lưu ý rằng vào thời điểm này, các mã dọn dẹp khác nhau, chẳng hạn như đóng các tệp đã được mở trong thử nghiệm, có thể đã được chạy. Các tùy chọn có thể là:

  1. Bạn có thể yêu cầu pytest thả bạn vào trình gỡ lỗi do lỗi bằng cách sử dụng tùy chọn --pdb .

  2. Bạn có thể định nghĩa trình trang trí sau và trang trí từng chức năng kiểm tra có liên quan với nó. (Ngoài đăng một tin nhắn, bạn cũng có thể bắt đầu một pdb.post_mortem vào thời điểm này, hoặc thậm chí là một tương tác code.interact với người dân địa phương của khung hợp ngoại lệ có nguồn gốc, như mô tả trong câu trả lời này .)

from functools import wraps

def pause_on_assert(test_func):
    @wraps(test_func)
    def test_wrapper(*args, **kwargs):
        try:
            test_func(*args, **kwargs)
        except AssertionError as e:
            tkinter.messagebox.showinfo(e)
            # re-raise exception to make the test fail
            raise
    return test_wrapper

@pause_on_assert
def test_abc()
    a = 10
    assert a == 2, "some error message"
  1. Nếu bạn không muốn trang trí thủ công mọi chức năng kiểm tra, thay vào đó, bạn có thể xác định một vật cố định tự động kiểm tra sys.last_value :
import sys

@pytest.fixture(scope="function", autouse=True)
def pause_on_assert():
    yield
    if hasattr(sys, 'last_value') and isinstance(sys.last_value, AssertionError):
        tkinter.messagebox.showinfo(sys.last_value)

Tôi thích câu trả lời với các nhà trang trí nhưng nó không thể được thực hiện một cách linh hoạt. Tôi muốn tự động kiểm soát khi tôi muốn tạm dừng_on_assert hay không. Có giải pháp nào cho điều này?
Nitesh

Năng động theo cách nào? Như trong một công tắc duy nhất để bật / tắt nó ở mọi nơi? Hoặc một số cách để kiểm soát nó cho mỗi bài kiểm tra?
Uri Granta

Giả sử tôi đang chạy một số testcase. Ở giữa tôi có nhu cầu tạm dừng về thất bại. Tôi sẽ kích hoạt công tắc. Sau này bất cứ lúc nào tôi cảm thấy rằng tôi cần phải tắt công tắc.
Nitesh

Trang trí của bạn trong câu trả lời: 2 sẽ không hoạt động với tôi vì trường hợp thử nghiệm của tôi sẽ có nhiều xác nhận
Nitesh

Về 'công tắc', bạn có thể cập nhật cách thực hiện pause_on_assertđể đọc từ tệp để quyết định có tạm dừng hay không.
Uri Granta

4

Một giải pháp đơn giản, nếu bạn sẵn sàng sử dụng Visual Studio Code, có thể sử dụng các điểm dừng có điều kiện .

Điều này sẽ cho phép bạn thiết lập các xác nhận của mình, ví dụ:

import pytest
def test_abc():
    a = 10
    assert a == 10, "some error message"

Sau đó, thêm một điểm dừng có điều kiện trong dòng khẳng định của bạn, nó sẽ chỉ bị phá vỡ khi xác nhận của bạn không thành công:

nhập mô tả hình ảnh ở đây


@Nitesh - Tôi nghĩ rằng giải pháp này giải quyết tất cả các vấn đề của bạn, bạn chỉ phá vỡ khi một xác nhận thất bại, bạn có thể gỡ lỗi mã ở đó và sau đó bạn có thể tiếp tục với các thử nghiệm còn lại sau đó ... Mặc dù nó hơi khó khăn hơn để thiết lập ban đầu
Nick Martin
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.