自动化测试框架pytest教程13-调试

调试测试失败简介

测试失败会发生。如果不发生,测试就没有什么用。当测试失败时,我们需要找出原因。这可能是测试的问题,也可能是应用的问题。确定问题出在哪里以及如何解决的过程是相似的。

我们将在 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 = 

    @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_cli = .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]。控制回溯的方式
  • -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 = 

    @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   = 
        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模块的pretty-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,然后重新运行失败的测试,我们可以看看这是否解决了问题。

你可能感兴趣的:(自动化测试框架pytest教程13-调试)