Home > database >  How can i test a void function in python using pytest?
How can i test a void function in python using pytest?

Time:11-27

I just started unit testing in python using pytest. Well, when I have a function with a return value, with the "assert" I can compare a certain value with the value that the function return. But if I had a void function that returns nothing and does a print at the end, for example:

def function() -> None:
    number = randint(0, 4)

    if (number == 0):
       print("Number 0")

    elif (number == 1):
       print("Number 1")

    elif (number == 2):
       print("Number 2")

    elif (number == 3):
       print("Number 3")

    elif (number == 4):
       print("Number 4")

How can i test this simple function to get 100% code coverage?

One method I've found to test this function is to do a return of the value (instead of print) and print it later, and then use the assert. But I wanted to know if it was possible to avoid this and do a test directly on the print statemant.

CodePudding user response:

You can redirect sys.stdout (the stream that print writes to) to a buffer and then examine or assert the contents of the buffer.

>>> import io
>>> import contextlib
>>> 
>>> def f():print('X')
... 
>>> buf = io.StringIO()
>>> with contextlib.redirect_stdout(buf):
...     f()
... 
>>> print(repr(buf.getvalue()))
'X\n'
>>> 
>>> buf.close()

(Recall that print() appends the value of its end argument to the line, which defaults to '\n').

CodePudding user response:

I suggest having a look at the plugin pytest-mock. It allows you to mock collaborating objects of your code under test.

Consider the following code under test:

# production.py 

def say_hello() -> None:
    print('Hello World.')

you can easily mock this now with

# production_test.py
from production import say_hello

def test_greeting(mocker):
    # The "mocker" fixture is auto-magicall inserted by pytest, 
    # once the extenson 'pytest-mock' is installed
    printer = mocker.patch('builtins.print')
    say_hello()
    assert printer.call_count == 1

You can also assert the arguments the printer function was called with, etc. You will find a lot of details in their useful documentation.


Now, consider you do not want to access the printer, but have a code with some undesirable side-effects (e.g. an operation takes forever, or the result is non-predictable (random).) Let's have another example, say

# deep_though.py

class DeepThought:
    #: Seven and a half million years in seconds
    SEVEN_HALF_MIO_YEARS = 2.366771e14

    @staticmethod
    def compute_answer() -> int:
        time.sleep(DeepThought.SEVEN_HALF_MIO_YEARS)
        return 42

yeah, I personally don't want my test suite to run 7.5 mio years. So, what do we do?

# deep_thought_test.py 
from deep_thought import DeepThought

def test_define_return_value(mocker) -> None:
    # We use the internal python lookup path to the method 
    # as an identifier (from the location it is called)      
    mocker.patch('deep_thought.DeepThought.compute_answer', return_value=12)
    assert DeepThought.compute_answer() == 12

Two more minor remarks, not directly related to the post:

  • A high code coverage (80% - 90%) is a good goal. I personally try to stck around 90-95%. However, 100% coverage is usually not necessary. Simple(!) data items and log-statements can usually be ignored.
  • It's a good practice to use a logger, instead of print. See Question 6918493 for example.
  • Related