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.
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
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: