Testing Your Code with Python’s pytest, Part II


Testing functions isn't hard, but how do you test user input and output?

In my last article, I started looking at "pytest", a framework for testing Python programs that's really changed the way I look at testing. For the first time, I really feel like testing is something I can and should do on a regular basis; pytest makes things so easy and straightforward.

One of the main topics I didn't cover in my last article is user input and output. How can you test programs that expect to get input from files or from the user? And, how can you test programs that are supposed to display something on the screen?

So in this article, I describe how to test input and output in a variety of ways, allowing you to test programs that interact with the outside world. I try not only to explain what you can do, but also show how it fits into the larger context of testing in general and pytest in particular.

User Input

Say you have a function that asks the user to enter an integer and then returns the value of that integer, doubled. You can imagine that the function would look like this:

def double():
    x = input("Enter an integer: ")
    return int(x) * 2

How can you test that function with pytest? If the function were to take an argument, the answer would be easy. But in this case, the function is asking for interactive input from the user. That's a bit harder to deal with. After all, how can you, in your tests, pretend to ask the user for input?

In most programming languages, user input comes from a source known as standard input (or stdin). In Python, sys.stdin is a read-only file object from which you can grab the user's input.

So, if you want to test the "double" function from above, you can (should) replace sys.stdin with another file. There are two problems with this, however. First, you don't really want to start opening files on disk. And second, do you really want to replace the value of sys.stdin in your tests? That'll affect more than just one test.

The solution comes in two parts. First, you can use the pytest "monkey patching" facility to assign a value to a system object temporarily for the duration of the test. This facility requires that you define your test function with a parameter named monkeypatch. The pytest system notices that you've defined it with that parameter, and then not only sets the monkeypatch local variable, but also sets it up to let you temporarily set attribute names.

In theory, then, you could define your test like this: