This post will deal with unit testing in Python using PyTest framework. If you want to know more about unit testing, or testing in general, please refer to this page.
Getting started with PyTest
Firstly, let us begin by installing PyTest. If you want to install PyTest in your virtual environment, please begin by switching to the environment.
To install with pip, use the following command:
pip install pytest
You can also install the code-coverage tool, which I will be explaining towards the end of the post.
pip install pytest-cov
Basic Usage
We write unit tests in function. That is, every test should be a function. All the test functions should begin with test_
or else, PyTest will not identify them as tests.
Let us see an example of a function that checks whether a number is odd. We create a file called is_odd.py
and write the following:
1 2 3 4 5 | def is_odd(x): if x % 2 == 1: return True else: return False |
You can write the test in the same file or a different file. For now, let’s write the test for it in the same file:
1 2 3 4 5 6 7 8 9 10 | import pytest def is_odd(x): if x % 2 == 1: return True else: return False def test_is_odd(): assert is_odd(5) is True |
Through the test, we are trying to assert that when an odd number (5) is passed to the function is_odd(), we get the result True. If it returns True, the test passes otherwise, the test fails.
To run the test, use the following command:
pytest
PyTest automatically detects all the tests from all the files and runs them. If you specifically want to run tests from one file, you will need to mention the file name too.
pytest is_odd.py
You could also run all the tests in a directory by providing the path:
pytest /project/
Once you run the test, you should see something like this:
====== 1 passed in 0.01s ======
Fixtures in PyTest
A Fixture is a convenient way, in PyTest, to pass a pre-initialized object or a variable to a test function. You can think of it as dependency injection. Fixtures really are just functions, which run before a test function to which it is applied.
Generally, fixtures are used in PyTest to do the following:
- To feed data to the test function
- To perform some task before and/or after running the test function (setup and teardown)
An example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import pytest def func_increment(x): """Function to increment x by 1 and return the value""" return x + 1 @pytest.fixture() def input_for_test(): """Fixture used as input for test_answer""" return 3 def test_answer(input_for_test): """Test for func_increment""" assert func_increment(input_for_test) == 4 |
To create a fixture, the decorator @pytest.fixture()
should be appended before a function definition. In the example above, input_for_test is a fixture which returns 3. We have then used the fixture in the test test_answer
. When you want to use a fixture in any test function, you will have to pass it as an argument to the test function. The name of the argument and the fixture should be exactly the same.
Using Fixture for setup and teardown
Consider a scenario where you want to initialize a class that connects to database, use the connection and dispose the connection after use. And you want to do this for multiple tests. If you do this in all the test functions, the initialization and the disposal will be repeated for all the tests.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class TestCustomerDataExtractor: def test_get_customer_name(self): db_object = SomeDBClass() result = db_object.get_customer_name(5) assert result == 'Mike' db_object.dispose() def test_get_customer_age(self): db_object = SomeDBClass() result = db_object.get_customer_age('Mike') assert result == 5 db_object.dispose() def test_get_customer_contact_number(self): db_object = SomeDBClass() result = db_object.get_customer_contact_number('Mike') assert result == '44153692' db_object.dispose() |
Instead of initializing SomeDBClass
and disposing it in every test function, you can use fixture and get rid of the repetitive lines.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class TestCustomerDataExtractor: @pytest.fixture() def db_object(): object = SomeDBClass() yield object #this will return the initialized object object.dispose() def test_get_customer_name(self, db_object): result = db_object.get_customer_name(5) assert result == 'Mike' def test_get_customer_age(self, db_object): result = db_object.get_customer_age('Mike') assert result == 5 def test_get_customer_contact_number(self, db_object): result = db_object.get_customer_contact_number('Mike') assert result == '44153692' |
Parametrize
Sometimes, we like to test a function multiple times for different parameters. For this, we generally write multiple calls to a function and assert multiple times.
For example, to test whether a function is_odd()
is working correctly:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import pytest def is_odd(num): if num % 2 == 1: return True else: return False def test_is_odd(): assert is_odd(5) == True assert is_odd(7) == True assert is_odd(10) == False assert is_odd(2) == False |
Instead of writing multiple assert statements, we can do it more easily using the parametrize feature of PyTest.
1 2 3 4 5 6 7 8 9 10 11 12 13 | #filename - is_odd.py import pytest def is_odd(num): if num % 2 == 1: return True else: return False @pytest.mark.parametrize("num, result", [(5,True),(7,True),(10,False),(10,False)]) def test_is_odd(num, result): assert is_odd(num) == result |
The decorator pytest.mark.parametrize
tells the test function to take the parameters (num and result) from the parameters provided in the decorator. The test then runs for all the parameters.
The output is as below:
1 2 3 4 | is_odd.py::test_is_odd[5-True] PASSED [ 25%] is_odd.py::test_is_odd[7-True] PASSED [ 50%] is_odd.py::test_is_odd[10-False0] PASSED [ 75%] is_odd.py::test_is_odd[10-False1] PASSED [100%] |
Code Coverage
To view the code coverage of the tests:
pytest --cov=filename
Or, if you want to check the code coverage for all the tests in directory:
pytest --cov=<path_to_directory>
Let us check the code coverage for the file is_odd.py that we created earlier.
1 2 3 4 5 6 7 8 9 10 | ===================================================== test session starts ====================================================== platform linux -- Python 3.6.8, pytest-5.3.2, py-1.8.0, pluggy-0.13.1 rootdir: /home/ashim/Desktop/byteknot plugins: cov-2.8.1 collected 4 items test_1.py …. [100%] ----------- coverage: platform linux, python 3.6.8-final-0 ----------- Name Stmts Miss Cover odd.py 7 1 86% ====================================================== 4 passed in 0.03s ======================================================= |
*****
If you have any questions or comments regarding the post (or something else), please feel free to reach out through the comments.
Testing Python Codes Using PyTest
Related posts
Today's pick
Categories
- Computer Vision/ML (3)
- Javascript (1)
- Linux (1)
- Python (20)
- Advance Python (3)
- Basic Python (6)
- Intermediate Python (11)
- Uncategorized (1)