Python How to start unit tests with pytest

eye-catchPython
Sponsored links

Writing unit tests is important for a project. There are many testing frameworks but this time I use pytest.

Sponsored links

Install pytest

Install pytest in your environment.

pip install pytest

There is a case where the pytest isn’t installed to the available PATH if you use a normal account AAA but needs a different admin account BBB to install Python. In this case, pytest will be downloaded to C:\Users\AAA\AppData\Roaming\Python\Python39\Scripts for example.

If you don’t get the version like below when executing the following command, you need to set the path to the windows environment variable.

$ pytest --version
pytest 7.1.3

How to add a path to the windows environment variable.

  1. Open Edit the system environment variables from windows menu
  2. Click Environment Variables... in Advanced tab
  3. Select Path from System variables
  4. Click Edit...
  5. Add the path where the script was downloaded
Sponsored links

Target file

Pytest runs the test cases in the following command.

pytest

If you don’t specify the target file name, pytest finds files whose name match test_*.py, and then run the test cases.

If you want to run the test for a specific file, you can specify the file name. The file name is not necessarilly test_*.py in this case.

pytest test_file.py

Target classes and functions for the test execution

Test function name must start with test. It is NOT Test.

test_sum   -> executed
Test_sum   -> NOT executed
tesT_sum   -> NOT executed
testable   -> executed
retest_sum -> NOT executed

If the test cases are defined in a class, the class name must start with Test.

TestMyTest  -> executed
Test_MyTest -> executed
ATestMyTest -> NOT executed

Here is an example defined in a class.

class TestMyTest():
    def test_sum(self):
        # write the test here

Test example

Success test

I wrote the following example.

# test_pytest.py
def sum(a, b):
    return a + b

def test_sum_one_plus_two():
    print("start test_sum_one_plus_two")
    result = sum(1, 2)
    assert result == 3
    print("end")

In the test function, we need to call the target function that we want to test. Then, the result needs to be tested by assert keyword.

Then, run the test.

$ pytest test_pytest.py 
======================================================================================= test session starts ========================================================================================
platform win32 -- Python 3.9.0, pytest-7.1.3, pluggy-1.0.0
rootdir: D:\Data\markdown\python\src
collected 1 item                                                                                                                                                                                     

test_pytest.py .                                                                                                                                                                              [100%] 

======================================================================================== 1 passed in 0.02s =========================================================================================

The test succeeded. In this case, pytest shows a dot after the test file name.

But it doesn’t show the print message.

How to show the print message

Add -s option if you want to show messages written by print() for the debugging purpose.

$ pytest test_pytest.py -s
======================================================================================================================= test session starts ========================================================================================================================
platform win32 -- Python 3.9.0, pytest-7.1.3, pluggy-1.0.0
rootdir: D:\Data\markdown\python\src
collected 1 item                                                                                                                                                                                                                                                     

test_pytest.py start test_sum_one_plus_two
end
.

======================================================================================================================== 1 passed in 0.01s =========================================================================================================================

“start test_sum_one_plus_two” and “end” are shown here.

Failure test

Let’s add a failure test to the same file.

def test_sum_fail():
    print("start test_sum_fail")
    result = sum(1, 2)
    assert result == 4
    print("end")

pytest shows the error test function name and the assertion.

$ pytest test_pytest.py -s
======================================================================================================================= test session starts ========================================================================================================================
platform win32 -- Python 3.9.0, pytest-7.1.3, pluggy-1.0.0
rootdir: D:\Data\markdown\python\src
collected 2 items                                                                                                                                                                                                                                                    

test_pytest.py start test_sum_one_plus_two
end
.start test_sum_fail
F

============================================================================================================================= FAILURES ============================================================================================================================= 
__________________________________________________________________________________________________________________________ test_sum_fail ___________________________________________________________________________________________________________________________ 

    def test_sum_fail():
        print("start test_sum_fail")
        result = sum(1, 2)
>       assert result == 4
E       assert 3 == 4

test_pytest.py:14: AssertionError
===================================================================================================================== short test summary info ====================================================================================================================== 
FAILED test_pytest.py::test_sum_fail - assert 3 == 4
=================================================================================================================== 1 failed, 1 passed in 0.06s ====================================================================================================================

The print message “start test_sum_fail” is shown but “end” is not shown because the assertion fails.

If you want to do a post-process, you need to add a try-finally block.

def test_sum_fail(before_after):
    print("start test_sum_fail")
    result = sum(1, 2)
    try:
        assert result == 4
    finally:
        print("end")

But writing try block for many tests is cumbersome. In this case, @pytest.fixture should be used. I will write about it in a different article. I will update here when it’s ready.

A case where the function throws an error

We can also test that the function throws an error in this way.

def div(a, b):
    return a / b

def test_div_raise_error():
    with pytest.raises(ZeroDivisionError):
        div(1, 0)

def test_div_raise_error2():
    with pytest.raises(NameError):
        div(1, 0)

The test fails if the error class is different from the specified one in the raises function.

$ pytest src/test_pytest.py
======================================================================================= test session starts ========================================================================================
platform win32 -- Python 3.9.0, pytest-7.1.3, pluggy-1.0.0
rootdir: D:\DMGMORI_Development\private\blogpost-python
collected 2 items                                                                                                                                                                                    

src\test_pytest.py .F                                                                                                                                                                         [100%]

============================================================================================= FAILURES ============================================================================================= 
______________________________________________________________________________________ test_div_raise_error2 _______________________________________________________________________________________ 

    def test_div_raise_error2():
        with pytest.raises(NameError):
>           div(1, 0)

src\test_pytest.py:34:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _  

a = 1, b = 0

    def div(a, b):
>       return a / b
E       ZeroDivisionError: division by zero

src\test_pytest.py:9: ZeroDivisionError
===================================================================================== short test summary info ====================================================================================== 
FAILED src/test_pytest.py::test_div_raise_error2 - ZeroDivisionError: division by zero
=================================================================================== 1 failed, 1 passed in 0.08s ====================================================================================

How to check the error value

If you need to refer the value of the error, add as <variable name>:.

def test_div_raise_error3():
    with pytest.raises(ZeroDivisionError) as err:
        div(1, 0)

    assert str(err.value) == "division by zero"

How to do parameterized test

There are many cases where a function needs to be tested with multiple test datasets. You can write as many test cases as you want but it’s easier to use @pytest.mark.parametrize.

By using this, all test datasets can be defined in one place.

import pytest

@pytest.mark.parametrize(
    argnames=["x", "y", "expected"],
    argvalues=[
        (1, 1, 2),
        (-1, 1, 0),
        (0, 0, 0),
    ]
)
def test_parameterized(x, y, expected):
    print(f"(x, y, expected) = ({x}, {y}, {expected})")
    result = sum(x, y)
    assert result == expected

In this way, one test definition tests multiple datasets. The name defined in argnames must be used in the test function arguments.

$ pytest test_pytest.py -s
================================================================================================= test session starts ================================================================================================== 
platform win32 -- Python 3.9.0, pytest-7.1.3, pluggy-1.0.0
rootdir: D:\Data\markdown\python\src
collected 3 items

test_pytest.py (x, y, expected) = (1, 1, 2)
.(x, y, expected) = (-1, 1, 0)
.(x, y, expected) = (0, 0, 0)
.

================================================================================================== 3 passed in 0.02s ===================================================================================================

Overview

Naming

  • File name: test_xxx.py
  • Class name: Testxxx
  • Function name: testxxx

Assertion

  • Normal value check: assert actual_value == expected_value
  • Error check: with pytest.raises(expected_error_class):

Test Execution

  • Test all targets: pytest
  • Test a specified file: pytest file_name
  • Show output message: pytest -s
  • Execute specified test: pytest file_name -k function_name

Comments

Copied title and URL