Basics¶
Level: Starter
Do I need unit tests?
In my opinion, yes.
For your reference, there are a lot of discussions on the internet:
And some arguments against unittest you may also want to know:
Just unit tests?
Not just unit tests, we also use pytest for integration testing, and E2E testing. So it's important
Objective¶
In this tutorial, you will learn how to:
- Write a simple test.
- Use
pytest.mark.parametrize
. - Test expected exceptions.
- View test code coverage.
Preparation¶
Install Pytest
$ pip install pytest
Installing pytest...
Write a Simple Test¶
Let's create two files coffee.py
and test_coffee.py
coffee.py | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
test_coffee.py | |
---|---|
1 2 3 4 5 6 |
|
You should have a folder structure as below:
.
├── coffee.py
└── test_coffee.py
In coffee.py
, we define a method to return coffee ingredients for several types of coffee, and raise an exception if an unsupported coffee type is encountered.
In test_coffee.py
, we test the method to confirm it returns correct ingredients for latte
.
Now let's run the test:
$ pytest test_coffee.py
collecting ...
test_coffee.py ✓ 100% ██████████
Results (0.03s):
1 passed
Write Multple Tests with parametrize
¶
Our coffee's get_ingredients
method can process several types of coffee, therefore, we need to add more test cases to cover more conditions.
parametrize
parametrize
helps pass different arguments to the test functions, making writing multple test cases more convenient.
Please read more information on parametrize
at pytest - parametrize
Update the test script to inlucde an extra coffee type: cappuccino
test_coffee.py | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
- Let's give this argument the name
coffee_type
,ingredients
. - Declare the argument.
Run the test script again.
$ pytest test_coffee.py
collecting ...
test_coffee.py ✓✓ 100% ██████████
Results (0.03s):
2 passed
As you can tell from the stdout
, there are totally two test cases now.
Can I use multiple parametrize
You can use multiple parametrize
within one test. Here is an example from parametrize:
import pytest
@pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3])
def test_foo(x, y):
pass
There will be 2x2=4 test cases.
Test the Exception¶
Perhaps you have already noticed, it is possible for the function to raise an exception. How do we test such case?
Test expected exceptions
Pytest provides pytest.raises
to solve such case, read more about it here.
Let's add another test to assert an expected exception.
test_coffee.py | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
$ pytest test_coffee.py
collecting ...
test_coffee.py ✓✓✓ 100% ██████████
Results (0.03s):
3 passed
Is there another way to assert an expected exception?
Yes. Here are some tasks for you to try.
Try different ways to assert an expected exception:
- pytest.raises(Exception, match=r"Unsupported coffee type: .*")
- pytest.mark.xfail
Test Coverage¶
First, install pytest-cov
.
$ pip install pytest-cov
Installing pytest-cov...
Now let's have look at the code coverage of our tests:
$ pytest --cov=coffee test_coffee.py
collecting ...
test_coffee.py ✓✓✓ 100% ██████████
-- coverage: platform darwin, python 3.8.9-final-0 --
Name Stmts Miss Cover
-------------------------------
coffee.py 10 2 80%
-------------------------------
TOTAL 10 2 80%
Results (0.05s):
3 passed
$ pytest --cov-report term-missing --cov=coffee test_coffee.py
collecting ...
test_coffee.py ✓✓✓ 100% ██████████
-- coverage: platform darwin, python 3.8.9-final-0 --
Name Stmts Miss Cover Missing
-----------------------------------------
coffee.py 10 2 80% 18, 20
-----------------------------------------
TOTAL 10 2 80%
Results (0.05s):
3 passed
$ pytest --cov-branch --cov-report term-missing --cov=coffee test_coffee.py
collecting ...
test_coffee.py ✓✓✓ 100% ██████████
--- coverage: platform darwin, python 3.8.9-final-0 --------
Name Stmts Miss Branch BrPart Cover Missing
------------------------------------------------------------
coffee.py 10 2 8 2 78% 18, 20
------------------------------------------------------------
TOTAL 10 2 8 2 78%
Results (0.05s):
3 passed
What is branch coverage?
From coverage:
Where a line in your program could jump to more than one next line, coverage.py tracks which of those destinations are actually visited, and flags lines that haven’t visited all of their possible destinations.
A naive case is:
branch.py | |
---|---|
1 2 3 4 5 |
|
test_branch.py | |
---|---|
1 2 3 4 5 |
|
Folder Structure
.
├── branch.py
└── test_branch.py
$ pytest --cov-branch --cov-report term-missing --cov=branch test_branch.py
collecting ...
test_branch.py ✓ 100% ██████████
--- coverage: platform darwin, python 3.8.9-final-0 ---
Name Stmts Miss Branch BrPart Cover Missing
-------------------------------------------------------
branch.py 5 0 2 1 86% 3->5
-------------------------------------------------------
TOTAL 5 0 2 1 86%
Results (0.05s):
1 passed