Mocking print() in Unit Tests
It can be difficult to write unit tests for methods like print()
that don’t return anything but have a side-effect of writing to the terminal. You want to ensure that what you expected to print to the terminal actually got printed to the terminal. The unittest.mock
library can help you test functions that have calls to print()
:
def greet(name):
print('Hello ', name)
from unittest.mock import patch
@patch('builtins.print')
def test_greet(mock_print):
# The actual test
greet('John')
mock_print.assert_called_with('Hello ', 'John')
greet('Eric')
mock_print.assert_called_with('Hello ', 'Eric')
# Showing what is in mock
import sys
sys.stdout.write(str( mock_print.call_args ) + '\n')
sys.stdout.write(str( mock_print.call_args_list ) + '\n')
00:00
In the previous lesson, I introduced you to simple animation using the \r
and \b
escape sequences. In this lesson, I’m going to talk about how to mock print()
if you’re testing code that has print()
inside of it. For any nontrivial-sized code, you should always build automated tests. Automated tests help you make sure your code has good quality. It’s faster than testing by hand, and it helps prevent regressive defects. As you modify your code as you move along, you can always rerun your tests to make sure that you haven’t broken anything.
00:31
print()
as a function has a side effect. Any function with side effects are more challenging to test. print()
’s side effect is displaying the output—that’s what we want it to do, but how are we sure that it works? Don’t get me wrong—you’re not trying to test print()
.
00:45
You’re trying to test code that calls print()
and make sure that the right thing is printed when your function is called. For the sake of this lesson, consider the following very simple method. greet()
prints out f'Hello '
, and the name
that is being passed in.
01:00
I’m going to show you different ways of dealing with this code and mocking print()
in order to handle it. The first technique I’m going to show you is something called dependency injection. In order to use dependency injection, you have to change how greet()
works. greet()
now takes two parameters—not just the name, but also a reference.
01:18
This reference defaults to the print()
function, which means display(f'Hi {name}')
would get passed to print()
. You’re going to use this in order to be able to mock out the value of display()
.
01:33
Lines 5 and 6 declare our mock_print()
. This is the fake version to use under test.
01:40
Let’s look at what happens when you call mock_print()
. Passing in 'Bob'
, you don’t get a return value and it doesn’t print anything.
01:48 Line 6 adds a new attribute to the function. Because functions are objects in Python, you can add attributes just as if it were a class. You can look at this attribute.
02:02 It was assigned the message that was passed in. Lines 9, 10, and 11 are our actual test function. If I call the test, you’ll notice that it actually fails.
02:14
It fails because line 10—passed in with mock_print()
—is going to call greet()
, greet()
will say f'Hi {name}'
. The expected result is f'Hello {name}'
instead, so our test has failed. Now I’m going to edit line 2 to change the string to say f'Hello '
, which is what it was supposed to do in the first place.
02:35
Rerun the test, and the test passes. No bells and whistles go off, but because the assert
passed, you don’t get the failure traceback and the test has passed. If you were using a test harness like pytest
, it would actually print out results showing you how many of your tests had passed.
02:56
One of the challenges with dependency injection is your code must be explicitly designed to use it. If you are trying to test existing code that doesn’t have the correct hooks, a lot of work is necessary in order to properly test. An alternative to this is to use the @patch
decorator inside of the mock
library that comes with Python.
03:17
Lines 1 and 2 are the original greet()
method without the dependency injection code. Line 5 imports patch
. Line 7 is a decorator wrapping builtins.print
.
03:31
What this decorator does is says for the duration of the functions associated with this test function, it’s going to replace the builtin print()
with a mock. That mock is passed in as the first argument to your test.
03:48
This means when greet()
is called, print()
is hit, mock
replaces by monkey patching print()
with the mock_print()
, and mock_print()
is called instead.
04:01
You can then use the .assert_called_with()
function on the mock_print
object to assert that 'Hello '
and 'John'
were passed as arguments to print()
.
04:10
If they were not, then it would fail—it raises an exception. Line 12, I’ve done it again with 'Eric'
. Lines 16 through 18 you wouldn’t normally have inside of a test, but asserts don’t show up when you call them, so I want to print some information out to show you how this works.
04:27
The reason I’m calling sys.stdout
instead of print()
is because I’ve already mocked print()
. If I call print()
to try to show you the output, it will call the mocked version and you won’t see anything. So, let me call test.
04:41 I called test. Lines 17 and 18 put the information on the console.
04:47
This first statement call with ('Hello ', 'Eric')
is from line 17. This is mock_print.call_args
. The .call_args
property shows the last arguments that mock_print()
was called with. Because ('Hello ', 'Eric')
was called second, the value down here says it was called with ('Hello ', 'Eric')
. mock_print.call_args_list
shows the entire call history. This can be really useful if you’ve got multiple calls going on—you can look at the mocked object and validate multiple arguments in one place.
05:18
mock
is a very, very powerful library. It allows you to do much deeper things than just this patch, and if you’re interested in doing good unit tests, I encourage you to look into this some more.
05:30
Hand in hand with testing is doing the debugging so you can create the tests in the first place. In the next lesson, I’ll show you how to use print()
to help you debug, and when you should be using logging instead.
Become a Member to join the conversation.