Writing unit tests is important for a project. There are many testing frameworks but this time I use pytest.
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.
- Open
Edit the system environment variables
from windows menu - Click
Environment Variables...
inAdvanced
tab - Select
Path
fromSystem variables
- Click
Edit...
- Add the path where the script was downloaded
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. Check the following post if you want to know how to share a function that does something as pre/post process.
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 ===================================================================================================
Check the following post too if you need to pass a list.
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