The ability to run your scripts through a debugger increases the speed and flexibility with which you can discern the behaviour of code in your software. Alongside this, by using tests you can easily run small sections of your code under controlled conditions. Combining these together gives you a great way to work out why parts of a larger program are perhaps not working as you expect.
The process for debugging your tests is almost exactly the same as for debugging scripts. Breakpoints and stepping, for example, work in the same way.
Let's work through an example to see what differences there are. Start by making a new file called
lists.py with the following contents:
def double_list(l): new_list = l for i in range(len(new_list)): new_list[i] *= 2 return new_list
Then make a second file,
test_lists.py to hold the tests:
from lists import double_list def test_double_list(): my_list = [1, 2, 3] doubled_list = double_list(my_list) assert doubled_list == [2, 4, 6] def test_double_list_relative(): my_list = [1, 2, 3] doubled_list = double_list(my_list) assert my_list * 2 == doubled_list
From a quick glance at the code, it looks like the tests should probably pass but let's run them to check.
Run all the tests and you should see that all the tests pass except
test_double_list_relative. This test is calling the
double_list function and then comparing the original list passed in to the returned list to check that it was doubled correctly.
Let's use the debugger to work out what's going wrong.
The process for running a test in debug mode is very similar to running a test normally.
Let's start by placing a breakpoint at a location that we think will be useful. Since it's likely that there's something wrong with the code inside
double_list, let's put the breakpoint in the test function, just at the point where that function is called. Put the breakpoint on line 13 of
doubled_list = double_list(my_list)).
There will usually be a button or option to start the debugger, near to that which you used for running a single test.
In PyCharm, when clicking the green arrow next to the test, there's a second option to
Debug 'pytest for test_list...':
In VS Code, next to the
Run Test button, there's a
Debug Test button:
Start the test in the debugger and it should pause execution on line 13 of
test_lists.py. The only variable defined at this point should be
my_list with the value
[1, 2, 3].
Since we've paused on a line with a function call, we can step into the function so go ahead and do that.
You'll now be sitting on line 2 of
lists.py with the variable
l (the function parameter) set to
[1, 2, 3]. This is same value as
my_list which makes sense as we passed it as an the argument to this function.
Step over to the next line so that you're paused on line 3. Now we see both
new_list shown in the variable list.
Step over once more to get to line 4. Now you'll see
i is also set. This variable is counting over the indices of
new_list so that each element can be updated one at a time. The line that we're paused on is going to update the
0th element of the list
new_list to be double its current value. We expect that if we step over then the value of
new_list in the variable list should update before our eyes.
While paying attention to the value of
new_list, step over to the next line of code. You'll now be on line 3 again since we're in a
for loop. The first element of
new_list has indeed been changed from
2 so the doubling seems to be working. However, look at the value of
l. This was our input argument for the function but it's been changed too!
This time keeping an eye on
l in the variable list, step over twice to get back to line 3. You'll see that once more both
new_list are changed. Clearly there's something causing the two variables to be linked together.
Place a breakpoint on line 5 of
lists.py and press the Resume/Continue button to jump out of the loop. Now, looking at the values of
l you'll see that they're both equal to
[2, 4, 6].
Step over or step out to get to line 15 of
test_lists.py. We're now sitting at the point where the
assert happens. Looking at the values of
doubled_list we can see that if you take the
0th element of
my_list and multiply it by two, it will not match the
0th element of
doubled_list so the
assert will indeed fail as the failing test shows.
Unlike when we were running our simple script through the debugger, our tests are being run for us by pytest. At this point if you keep on stepping over or stepping out will will end up in code from pytest. Now we've finished investigating what's going on with the variable and seen that the two lists inside
double_list are incorrectly linked we can stop the debugger by pressing the red square stop button.
Now we understand the mechanics of the function a little better, we can start fixing it. A debugger doesn't magically tell us the answer, it is simply a tool to provide us with information.
In Python when you edit one variable and it changes another, it is usually caused by a mistake in copying a variable. Python's
= doesn't copy the data on the right-hand side, it creates a new name for the same data. So in
double_list the variables
new_list are pointing at exactly the same data behind the scenes. This is why changing one changes the other.
The second cause is that when we passed
my_list to the function, internally it is referring to it as
l. In some programming languages it would have taken a copy of
my_list for the function's use but in Python the variables inside refer to the same data as outside. This means than when
l is changed, so is
Therefore, by line 3 of
lists.py, we have three names for the same piece of data:
new_list and changing any one of them will change the rest.