调试测试失败简介
测试失败会发生。如果不发生,测试就没有什么用。当测试失败时,我们需要找出原因。这可能是测试的问题,也可能是应用的问题。确定问题出在哪里以及如何解决的过程是相似的。
我们将在 pytest 标志和 pdb 的帮助下调试一些失败的代码
调试测试失败简介
增加如下新功能:cards list -state done
# 安装新版本
$ cd ch13/cards_proj
$ pip install -e .
$ pytest tests
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick\ch13\cards_proj, configfile: pytest.ini, testpaths: tests
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0, cov-4.0.0
collected 53 items
tests\api\test_add.py ..... [ 9%]
tests\api\test_config.py . [ 11%]
tests\api\test_count.py ... [ 16%]
tests\api\test_delete.py ... [ 22%]
tests\api\test_finish.py .... [ 30%]
tests\api\test_list.py ......... [ 47%]
tests\api\test_list_done.py F [ 49%]
tests\api\test_start.py .... [ 56%]
tests\api\test_update.py .... [ 64%]
tests\api\test_version.py . [ 66%]
tests\cli\test_add.py .. [ 69%]
tests\cli\test_config.py .. [ 73%]
tests\cli\test_count.py . [ 75%]
tests\cli\test_delete.py . [ 77%]
tests\cli\test_done.py F [ 79%]
tests\cli\test_errors.py ..... [ 88%]
tests\cli\test_finish.py . [ 90%]
tests\cli\test_list.py .. [ 94%]
tests\cli\test_start.py . [ 96%]
tests\cli\test_update.py . [ 98%]
tests\cli\test_version.py . [100%]
================================== FAILURES ===================================
_______________________________ test_list_done ________________________________
cards_db = <cards.api.CardsDB object at 0x00000202D789C670>
@pytest.mark.num_cards(10)
def test_list_done(cards_db):
cards_db.finish(3)
cards_db.finish(5)
the_list = cards_db.list_done_cards()
> assert len(the_list) == 2
E TypeError: object of type 'NoneType' has no len()
tests\api\test_list_done.py:11: TypeError
__________________________________ test_done __________________________________
cards_db = <cards.api.CardsDB object at 0x00000202D789C670>
cards_cli = <function cards_cli_no_redirect.<locals>.run_cli at 0x00000202D7A34040>
def test_done(cards_db, cards_cli):
cards_db.add_card(cards.Card("some task", state="done"))
cards_db.add_card(cards.Card("another"))
cards_db.add_card(cards.Card("a third", state="done"))
output = cards_cli("done")
> assert output == expected
E AssertionError: assert '' == '\n ID sta... a third'
E -
E - ID state owner summary
E - ????????????????????????????????????????????????????????????????
E - 1 done some task
E - 3 done a third
tests\cli\test_done.py:16: AssertionError
=========================== short test summary info ===========================
FAILED tests/api/test_list_done.py::test_list_done - TypeError: object of typ...
FAILED tests/cli/test_done.py::test_done - AssertionError: assert '' == '\n ...
======================== 2 failed, 51 passed in 1.95s =========================
pytest标志
-lf / --last-failed:/ -最后一次失败。只运行最后失败的测试
-ff / --先失败的。运行所有的测试,从最后失败的测试开始。
-x / --exitfirst: 在第一次失败后停止测试会话。
--maxfail=num: 在制定次数失败后停止测试
-nf / --new-first: 运行所有的测试,按文件修改时间排序
--sw / --stepwise: 在第一次失败时停止测试。下次在最后一次失败时启动测试
--sw-skip / --stepwise-skip。与-sw相同,但跳过第一次失败。
控制pytest输出的标志。
- -v / --verbose。显示所有的测试名称,不管是通过的还是失败的
- --tb=[auto/long/short/line/native/no]??刂苹厮莸姆绞?/li>
- -l / --showlocals: 在堆栈跟踪的同时显示局部变量。
启动命令行调试器的标志。
--pdb: 在故障点启动交互式调试会话
--trace。在运行每个测试时立即启动pdb源代码调试器
--pdbcls: 使用pdb的替代品,例如IPython的调试器,使用-pdbcls=IPython.terminal.debugger:TerminalPdb
重新运行失败的测试
让我们开始我们的调试,确保当我们再次运行测试时失败。我们将使用 --lf 来重新运行失败的测试,而 --tb=no 来隐藏回溯,因为我们还没有准备好。
$ pytest --lf --tb=no
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick\ch13\cards_proj, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0, cov-4.0.0
collected 27 items / 25 deselected / 2 selected
run-last-failure: rerun previous 2 failures (skipped 13 files)
api\test_list_done.py F [ 50%]
cli\test_done.py F [100%]
=========================== short test summary info ===========================
FAILED api\test_list_done.py::test_list_done - TypeError: object of type 'Non...
FAILED cli\test_done.py::test_done - AssertionError: assert '' == '\n ID s...
====================== 2 failed, 25 deselected in 0.23s =======================
让我们只运行第一个失败的测试,在失败后停止,然后看一下回溯。
$ pytest --lf -x
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick\ch13\cards_proj, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0, cov-4.0.0
collected 27 items / 25 deselected / 2 selected
run-last-failure: rerun previous 2 failures (skipped 13 files)
api\test_list_done.py F
================================== FAILURES ===================================
_______________________________ test_list_done ________________________________
cards_db = <cards.api.CardsDB object at 0x000002C251B1A460>
@pytest.mark.num_cards(10)
def test_list_done(cards_db):
cards_db.finish(3)
cards_db.finish(5)
the_list = cards_db.list_done_cards()
> assert len(the_list) == 2
E TypeError: object of type 'NoneType' has no len()
api\test_list_done.py:11: TypeError
=========================== short test summary info ===========================
FAILED api\test_list_done.py::test_list_done - TypeError: object of type 'Non...
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!
====================== 1 failed, 25 deselected in 0.27s =======================
为了确保我们了解问题,我们可以用-l/--showlocals重新运行同一个测试。我们不需要完整的回溯,所以我们可以用--tb=short来缩短它。
$ pytest --lf -x -l --tb=short
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick\ch13\cards_proj, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0, cov-4.0.0
collected 27 items / 25 deselected / 2 selected
run-last-failure: rerun previous 2 failures (skipped 13 files)
api\test_list_done.py F
================================== FAILURES ===================================
_______________________________ test_list_done ________________________________
api\test_list_done.py:11: in test_list_done
assert len(the_list) == 2
E TypeError: object of type 'NoneType' has no len()
cards_db = <cards.api.CardsDB object at 0x000002365B936670>
the_list = None
=========================== short test summary info ===========================
FAILED api\test_list_done.py::test_list_done - TypeError: object of type 'Non...
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!
====================== 1 failed, 25 deselected in 0.27s =======================
没错,the_list = None。-l/--showlocals通常是非常有用的,有时足以完全调试出一个测试失败。更重要的是,-l/--showlocals的存在已经训练了我在测试中使用大量的中间变量。当测试失败时,它们就会派上用场。
现在我们知道,在这种情况下,list_done_cards()返回的是 None。但我们不知道为什么。我们将在测试过程中使用 pdb 来调试 list_done_cards() 的内部。
参考资料
- 本文涉及的python测试开发库 谢谢点赞!
- 本文相关海量书籍下载
用 pdb 调试
pdb, "Python 调试器 "(Python debugger)的缩写,是 Python 标准库的一部分。
你可以通过几种不同的方式从pytest启动pdb。
在测试代码或应用代码中添加breakpoint() 调用。
使用--pdb标志。使用-pdb,pytest将在故障点处停止。
使用--trace标志。使用--trace,pytest将在每个测试的开始处停止。
对于我们的目的来说,将--lf和--trace结合起来,效果会非常好。这个组合将告诉pytest重新运行失败的测试,并在test_list_done()的开始处停止,在调用list_done_cards()之前。
$ pytest --lf --trace
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick\ch13\cards_proj, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0, cov-4.0.0
collected 27 items / 25 deselected / 2 selected
run-last-failure: rerun previous 2 failures (skipped 13 files)
api\test_list_done.py
>>>>>>>>>>>>>>>>>>>> PDB runcall (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>>
> d:\code\pytest_quick\ch13\cards_proj\tests\api\test_list_done.py(6)test_list_done()
-> cards_db.finish(3)
(Pdb)
以下是pdb识别的常用命令。完整的列表在pdb文档中。
-
元命令:
- h(elp)。打印一个命令的列表
- h(elp)命令。打印一个命令的帮助
- q(uit): 退出pdb
-
查看所在的位置。
- l(ist) : 列出当前行周围的11行。再次使用它可以列出下一个11行,以此类推。
- l(ist) . : 和上面一样,但有一个点。列出当前行周围的11行。如果你用了几次l(list)而失去了当前的位置,就会很方便。
- l(ist) first, last: 列出一组特定的行
- ll : 列出当前函数的所有源代码
- w(here): 打印堆栈跟踪
-
查看数值。
- p(rint) expr: expr并打印其值
- pp expr:与p(rint) expr相同,但使用pprint??榈膒retty-print。非常适用于结构
- a(rgs)。打印当前函数的参数列表
-
执行命令。
- s(tep): 在你的源代码中执行当前行并跳到下一行,即使它是在一个函数中。
- n(ext): 执行当前行并跳到当前函数的下一行
- r(eturn): 继续执行,直到当前函数返回
- c(ontinue): 继续执行,直到下一个断点。当与-trace一起使用时,继续到下一个测试的开始。
- unt(il) lineno: 持续到给定的行号
继续调试我们的测试,我们将使用ll来列出当前的函数。
$ pytest --lf --trace
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick\ch13\cards_proj, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0, cov-4.0.0
collected 27 items / 25 deselected / 2 selected
run-last-failure: rerun previous 2 failures (skipped 13 files)
api\test_list_done.py
>>>>>>>>>>>>>>>>>>>> PDB runcall (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>>
> d:\code\pytest_quick\ch13\cards_proj\tests\api\test_list_done.py(6)test_list_done()
-> cards_db.finish(3)
(Pdb) ll
4 @pytest.mark.num_cards(10)
5 def test_list_done(cards_db):
6 -> cards_db.finish(3)
7 cards_db.finish(5)
8
9 the_list = cards_db.list_done_cards()
10
11 assert len(the_list) == 2
12 for card in the_list:
13 assert card.id in (3, 5)
14 assert card.state == "done"
(Pdb) until 8
> d:\code\pytest_quick\ch13\cards_proj\tests\api\test_list_done.py(9)test_list_done()
-> the_list = cards_db.list_done_cards()
(Pdb) step
--Call--
> d:\code\pytest_quick\ch13\cards_proj\src\cards\api.py(91)list_done_cards()
-> def list_done_cards(self):
(Pdb) ll
91 -> def list_done_cards(self):
92 """Return the 'done' cards."""
93 done_cards = self.list_cards(state="done")
(Pdb) return
--Return--
> d:\code\pytest_quick\ch13\cards_proj\src\cards\api.py(93)list_done_cards()->None
-> done_cards = self.list_cards(state="done")
(Pdb) ll
91 def list_done_cards(self):
92 """Return the 'done' cards."""
93 -> done_cards = self.list_cards(state="done")
(Pdb) pp done_cards
[Card(summary='Line for PM identify decade.', owner='Russell', state='done', id=3),
Card(summary='Director baby season industry the describe.', owner='Cody', state='done', id=5)]
(Pdb) step
> d:\code\pytest_quick\ch13\cards_proj\tests\api\test_list_done.py(11)test_list_done()
-> assert len(the_list) == 2
(Pdb) ll
4 @pytest.mark.num_cards(10)
5 def test_list_done(cards_db):
6 cards_db.finish(3)
7 cards_db.finish(5)
8
9 the_list = cards_db.list_done_cards()
10
11 -> assert len(the_list) == 2
12 for card in the_list:
13 assert card.id in (3, 5)
14 assert card.state == "done"
(Pdb) pp the_list
None
(Pdb) exit
!!!!!!!!!!!!!!!!!! _pytest.outcomes.Exit: Quitting debugger !!!!!!!!!!!!!!!!!!!
===================== 25 deselected in 172.96s (0:02:52) ======================
现在很清楚了。我们在list_done_cards()中的ded_cards变量中得到了正确的列表。然而,这个值并没有返回。因为如果没有返回语句,Python 的默认返回值是 None,这就是 test_list_done() 中被分配给 the_list 的值。
如果我们停止调试器,在 list_done_cards() 中添加返回值 done_cards,然后重新运行失败的测试,我们可以看看这是否解决了问题。