The pdb module is the Python debugger in the standard library. You use --pdb to have pytest start a debugging session at the point of failure. Let’s look at pdb in action in the context of the Tasks project.
In Parametrizing Fixtures, we left the Tasks project with a few failures:
| $ cd /path/to/code/ch3/c/tasks_proj |
| $ pytest --tb=no -q |
| .........................................FF.FFFF |
| FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF.FFF........... |
| 42 failed, 54 passed in 4.74 seconds |
Before we look at how pdb can help us debug this test, let’s take a look at the pytest options available to help speed up debugging test failures, which we first looked at in Using Options:
Installing MongoDB | |
---|---|
As mentioned in Chapter 3, pytest Fixtures, running the MongoDB tests requires installing MongoDB and pymongo. I’ve been testing with the Community Server edition found at https://www.mongodb.com/download-center. pymongo is installed with pip: pip install pymongo. However, this is the last example in the book that uses MongoDB. To try out the debugger without using MongoDB, you could run the pytest commands from code/ch2/, as this directory also contains a few failing tests. |
We just ran the tests from code/ch3/c to see that some of them were failing. We didn’t see the tracebacks or the test names because --tb=no turns off tracebacks, and we didn’t have --verbose turned on. Let’s re-run the failures (at most three of them) with verbose:
| $ pytest --tb=no --verbose --lf --maxfail=3 |
| ===================== test session starts ====================== |
| run-last-failure: rerun last 42 failures |
| collected 96 items |
| |
| tests/func/test_add.py::test_add_returns_valid_id[mongo] FAILED |
| tests/func/test_add.py::test_added_task_has_id_set[mongo] FAILED |
| tests/func/test_add_variety.py::test_add_1[mongo] FAILED |
| |
| !!!!!!!!!!!! Interrupted: stopping after 3 failures !!!!!!!!!!!! |
| ===================== 54 tests deselected ====================== |
| =========== 3 failed, 54 deselected in 3.14 seconds ============ |
Now we know which tests are failing. Let’s look at just one of them by using -x, including the traceback by not using --tb=no, and showing the local variables with -l:
| $ pytest -v --lf -l -x |
| ===================== test session starts ====================== |
| run-last-failure: rerun last 42 failures |
| collected 96 items |
| |
| tests/func/test_add.py::test_add_returns_valid_id[mongo] FAILED |
| |
| =========================== FAILURES =========================== |
| _______________ test_add_returns_valid_id[mongo] _______________ |
| |
| tasks_db = None |
| |
| def test_add_returns_valid_id(tasks_db): |
| """tasks.add(<valid task>) should return an integer.""" |
| # GIVEN an initialized tasks db |
| # WHEN a new task is added |
| # THEN returned task_id is of type int |
| new_task = Task('do something') |
| task_id = tasks.add(new_task) |
| > assert isinstance(task_id, int) |
| E AssertionError: assert False |
| E + where False = isinstance(ObjectId('59783baf8204177f24cb1b68'), int) |
| |
| new_task = Task(summary='do something', owner=None, done=False, id=None) |
| task_id = ObjectId('59783baf8204177f24cb1b68') |
| tasks_db = None |
| |
| tests/func/test_add.py:16: AssertionError |
| !!!!!!!!!!!! Interrupted: stopping after 1 failures !!!!!!!!!!!! |
| ===================== 54 tests deselected ====================== |
| =========== 1 failed, 54 deselected in 2.47 seconds ============ |
Quite often this is enough to understand the test failure. In this particular case, it’s pretty clear that task_id is not an integer—it’s an instance of ObjectId. ObjectId is a type used by MongoDB for object identifiers within the database. My intention with the tasksdb_pymongo.py layer was to hide particular details of the MongoDB implementation from the rest of the system. Clearly, in this case, it didn’t work.
However, we want to see how to use pdb with pytest, so let’s pretend that it wasn’t obvious why this test failed. We can have pytest start a debugging session and start us right at the point of failure with --pdb:
| $ pytest -v --lf -x --pdb |
| ===================== test session starts ====================== |
| run-last-failure: rerun last 42 failures |
| collected 96 items |
| |
| tests/func/test_add.py::test_add_returns_valid_id[mongo] FAILED |
| >>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>> |
| |
| tasks_db = None |
| |
| def test_add_returns_valid_id(tasks_db): |
| """tasks.add(<valid task>) should return an integer.""" |
| # GIVEN an initialized tasks db |
| # WHEN a new task is added |
| # THEN returned task_id is of type int |
| new_task = Task('do something') |
| task_id = tasks.add(new_task) |
| > assert isinstance(task_id, int) |
| E AssertionError: assert False |
| E + where False = isinstance(ObjectId('59783bf48204177f2a786893'), int) |
| |
| tests/func/test_add.py:16: AssertionError |
| >>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>> |
| > /path/to/code/ch3/c/tasks_proj/tests/func/test_add.py(16) |
| > test_add_returns_valid_id() |
| -> assert isinstance(task_id, int) |
| (Pdb) |
Now that we are at the (Pdb) prompt, we have access to all of the interactive debugging features of pdb. When looking at failures, I regularly use these commands:
p/print expr: Prints the value of exp.
pp expr: Pretty prints the value of expr.
l/list: Lists the point of failure and five lines of code above and below.
l/list begin,end: Lists specific line numbers.
a/args: Prints the arguments of the current function with their values. (This is helpful when in a test helper function.)
u/up: Moves up one level in the stack trace.
d/down: Moves down one level in the stack trace.
q/quit: Quits the debugging session.
Other navigation commands like step and next aren’t that useful since we are sitting right at an assert statement. You can also just type variable names and get the values.
You can use p/print expr similar to the -l/--showlocals option to see values within the function:
| (Pdb) p new_task |
| Task(summary='do something', owner=None, done=False, id=None) |
| (Pdb) p task_id |
| ObjectId('59783bf48204177f2a786893') |
| (Pdb) |
Now you can quit the debugger and continue on with testing.
| (Pdb) q |
| |
| |
| !!!!!!!!!!!! Interrupted: stopping after 1 failures !!!!!!!!!!!! |
| ===================== 54 tests deselected ====================== |
| ========== 1 failed, 54 deselected in 123.40 seconds =========== |
If we hadn’t used -x, pytest would have opened pdb again at the next failed test. More information about using the pdb module is available in the Python documentation.[20]
18.220.139.168