3 minute read

Here I summarized how to test my code using unittest, a built-in library for code test in python, and how to check code coverage using coverge package.

Create Unit Tests and Execute using unittest

I wrote some math-library as follows. Having functions for elementary algebraic operation defined in math.py, I want to test them using test.py script.

1
2
3
4
mathlibrary
 ├─ __init__.py
 ├─ math.py
 └─ test.py

Write Test Code

I wrote math.py first. The code looks like this.

1
2
3
4
5
6
7
8
9
10
11
def add(a: float, b: float) -> float:
    return a + b

def subtract(a: float, b: float) -> float:
    return a - b

def multiply(a: float, b: float) -> float:
    return a*b

def divide(a: float, b: float) -> float:
    return a/b

Write Unit Test Classes and Methods

Now, I need to create my unit tests in test.py. Note that there is a basic class TestCase which I should inherit when I make my own test class. Individual tests are defined as method under my custom class for testing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from unittestandcoverage import math
import unittest

class MathFunctionTest(unittest.TestCase):
    
    def test_add(self):
        self.assertEqual(math.add(2, 3), 5)

    def test_subtract(self):
        self.assertEqual(math.subtract(6, 3), 3)
    
    def test_multiply(self):
        self.assertEqual(math.multiply(7, 8), 56)

    def test_division(self):
        self.assertEqual(math.divide(6, 2), 3)

    def test_division_by_zero(self):
        with self.assertRaises(ZeroDivisionError):
            math.divide(6, 0)

if __name__ == '__main__':
    unittest.main()

I imported my custom math module, which I want to test. And also imported unittest module, to allow my custom classes for test to inherit from unittest.TestCase class.

I built MathFunctionTest custom class and rendered 4 different methods, each is assigned to test one of total 4 functions I defined in math module. Parent TestCase has methods designed to check whether a result of executing a function is correct or not in several different ways. I used assertEqual method for my example.

And I should also check some cornercases. For example, math.divide must throw zero-division error if given denominator is zero. To see if my math.divide handles well such case, I assigned a separate test function for this (test_division_by_zero) and used assertRaise method to see if ZeroDivisionError is raised as intened when 0 is cast as denominator.

Here, you can see that there is no restriction like ‘The number of test functions should equal to the number of functions to be tested’.

Now when I run test.py, all 5 tests are conducted and report is displayed to show how many of them succeeds.

1
2
3
4
5
6
PS D:\repositories\devlog_codes>python test.py
.....
----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK

Check Test Coverage with coverage

I also can get statistic report on the portion of codebase covered by the tests I wrote. coverage package supports this.

Install coverage

If you don’t have coverage package yet you need to install it first by running pip install coverage command.

1
2
3
4
PS D:\repositories\devlog_codes> pip install coverage
Collecting coverage
  Downloading coverage-7.3.2-cp310-cp310-win_amd64.whl (203 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 203.2/203.2 kB 6.2 MB/s eta 0:00:00

Check Code Coverage

Now I can run coverage run test.py in command prompt to get the code coverage statistics with test.py.

1
2
3
4
5
PS D:\repositories\devlog_codes> coverage run D:\repositories\devlog_codes\unittestandcoverage\test.py
.....
----------------------------------------------------------------------

OK

After you see OK, summary of report is available with coverage report command.

1
2
3
4
5
6
7
8
9
OK
PS D:\repositories\devlog_codes> coverage report
Name                              Stmts   Miss  Cover
-----------------------------------------------------
unittestandcoverage\__init__.py       0      0   100%
unittestandcoverage\math.py           8      0   100%
unittestandcoverage\test.py          20      0   100%
-----------------------------------------------------
TOTAL                                28      0   100%

pytest

pytest is also a framework you can use to test your codes. You need python 3.8 or higher to use pytest.

Installation

1
pip install -U pytest

Usage

There are some rules in nomenclatures for the files used for testing using pytest:

  • File name takes the form of test_*.py or *_test.py
  • Class name takes the form of Test*
  • Class method/function names take the form of test_*

To run the test, there are several ways you can trigger it.

  • pytest command runs all test files in current directory.
  • pytest <dir-name>/ runs all test files in <dir-name> directory
  • pytest <file-name> runs specific test file named <file-name>

Fixtures

In testing your codes, you get to repeatedly find that your tests including some necessary but redundant portion. For example, you may need to load some raw data file repeatedly. Or you may need to create instances of a class before running a test.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import pytest

# Import custom class, a mighty Dragon, for testing
# Instances of Dragon can fire_breath() and fly()
# fire_breath returns string "fire"
# fly returns stirng "fly"
from my_project import Dragon

@pytest.fixture
def summon_dragon():
	dragon = Dragon()
	return dragon

def test_fire_breath(summon_dragon):
	assert summon_dragon.fire_breath() == "breath"

def test_fly(summon_dragon):
	assert summon_dragon.fly() == "fly"

By decorating summon_dragon as fixture, repetition of creating dragon instance can be avoided. You can pass the fixture function summon_dragon as an argument of your test functions, which automatically enables your test function to access dragon inside of itself.

Without using fixture concept, the tests had to be like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import pytest

# Import custom class, a mighty Dragon, for testing
# Instances of Dragon can fire_breath() and fly()
# fire_breath returns string "fire"
# fly returns stirng "fly"
from my_project import Dragon

def summon_dragon():
	dragon = Dragon()
	return dragon

def test_fire_breath():
	assert summon_dragon().fire_breath() == "breath"

def test_fly():
	assert summon_dragon().fly() == "fly"

You can also chain multiple fixtures.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@pytest.fixture
def on_plate():
    return []

@pytest.fixture
def add_icecream(on_plate):
    on_plate.append("Icecream")

@pytest.fixture(autouse=True)
def add_waffle(on_plate, add_icecream):
    on_plate.append("Waffle")

def test_on_plate(on_plate):
    assert on_plate == ["Icecream", "Waffle"]	

Notice autouse argument of fixture add_waffle. This argument enables add_waffle to be run without explicitly using it on our test functoin test_on_plate. As add_waffle includes another fixture add_icecream, it is called first and the string "Icecream" is appended to empty list (which is called by on_plate) before the other string "Waffle" is appended.

A noteworthy point about chained fixtures is that how pytest deals with the case if any one of the fixtures chained together contains error (and therefore raise some type of error). It is said that pytest does not attempt to run any test any of the fixtures for which contain error. As the test is not attempted to be executed, we cannot interpret failure in such case as a failed test.

Scope of Fixture

We can set scope for fixtures.

1
@pytest.fixture(scope="<scope>")

<scope> can be one of below, where the default setting is function.

  • function
  • class
  • module
  • package
  • session

If the scope of a fixture is session, for example, that fixture is called only once during a given test session (i.e. when you run test file using pytest <filename>.py in command prompt)