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; assert
tăng AssertionError
như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...except
khố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 --pdb
cô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 q
lệ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 pdb
trì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 --pdbcls
tắ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 -s
chuyể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, pudb
bạ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 Pdb
mà 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. pytest
Sử dụng Pdb()
giống hệt như pdb.post_mortem()
thực hiện :
p = Pdb()
p.reset()
p.interaction(None, t)
Ở đây, t
là một đối tượng truy nguyên . Khi p.interaction(None, t)
trả về, pytest
tiế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 TerminalReporter
plugin 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_interact
hook 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ý pdbinvoke
plugin 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_ham
lỗi thử nghiệm bị bỏ qua, chỉ có test_spam
lỗ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
ExceptionInfo
thể hiện).
Lưu ý rằng các plugin gỡ lỗi pytest cụ thể (như pytest-pudb
hoặc pytest-pycharm
) đăng ký pytest_exception_interact
hooksp 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_plugin
và hasattr()
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ề None
hoặ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...except
xử 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.Exception
và pytest.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 a
cờ để 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()
và 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 assert
cá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 assert
câ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.py
mã 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 assert
vớ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 foo
là không None
, thì sau đó khẳng định sẽ dựa vào foo.bar
sự 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, assert
là 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.Assert
nút bên trong một ast.Try
nút và đính kèm một trình xử lý ngoại trừ sử dụng ast.Raise
nú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 AssertionError
ngoạ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
Pdb
lớ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à c
tiế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.Assert
cá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 jump
lệ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 đề.