pdb: Debugging Test Failures

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:

  • --tb=[auto/long/short/line/native/no]: Controls the traceback style.
  • -v / --verbose: Displays all the test names, passing or failing.
  • -l / --showlocals: Displays local variables alongside the stacktrace.
  • -lf / --last-failed: Runs just the tests that failed last.
  • -x / --exitfirst: Stops the tests session with the first failure.
  • --pdb: Starts an interactive debugging session at the point of failure.

Installing MongoDB

images/aside-icons/note.png

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]

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.139.168