Python Unit Test Python

Feb 28th, 2020 - written by Kimserey with .

Unit tests ensure that the functionalities of the Software are maintained while developping new features. Every language provides library for writing unit tests, and any decent framework provides facilities to enable easy unit testing. In Python the core comes packed with unittest, a module providing all the necessary functuonalities to unit test our application. In today’s post we will look at how we can setup unittest for our Python application.

Setting up unittest

In order to create unit tests, we start first by creating a tests module.

1
2
3
/tests
  __init__.py
  __main__.py

And add the following in __main__.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
"""Main"""

import os
import unittest


def suite():
    """Discover test suite."""
    return unittest.defaultTestLoader.discover(
        os.path.dirname(os.path.realpath(__file__))
    )


def run():
    """Run the test suite."""
    runner = unittest.TextTestRunner(verbosity=2)
    test_result = runner.run(suite())
    if test_result.errors or test_result.failures:
        raise RuntimeError("There are test failures")

if __name__ == "__main__":
    run()

The defaultTestLoader allows us to discover the tests available in the module. Any test file matching the pattern test* will be scanned.

We then use the TextTestRunner to run our tests in the shell with a text output.

We can then run our tests with the following command:

1
2
3
4
5
6
$> python3 -m tests

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

Having a tests module setup this way will avoid us having to explicitely specify modules to be tested by unittest. If we wanted to do so, we could have use directly unittest:

1
python3 -m unittest tests.test_user

Or we could have done the following if we imported all our test cases in __init__.py:

1
python3 -m unittest tests

Regardless, the tests module provides us a easy way to discover test cases and run them. We now have a test runner working properly. The next step is to actually build test cases.

Test Cases

Test cases are created by inheriting from TestCase class.

1
from unittest import TestCase

We start by creating a test case file test_user.py with the following content:

1
2
3
4
5
6
7
8
from unittest import TestCase

class TestUser(TestCase):
    """User Test Case"""

    def test_user_can_login(self):
        """Test that the user can login."""
        self.assertTrue(True)

and each test is created by creating a function with a prefix test_.

1
2
3
4
5
6
7
8
9
$> py -m tests

test_user_can_login (test_user.TestUser)
Test that the user can login. ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

When an assertion fails, a Failure will be reported while if an exception occurs an Error will be reported.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$> py -m tests

test_failure (test_user.TestUser)
Example of test failure. ... FAIL
test_user_can_login (test_user.TestUser)
Test that the user can login. ... ok

======================================================================
FAIL: test_failure (test_user.TestUser)
Example of test failure.
----------------------------------------------------------------------
Traceback (most recent call last):
  File, line 12, in test_failure
    self.assertEqual(1, 2)
AssertionError: 1 != 2

We also have access to decorators like skip and expectedFailure. @unittest.skip("reason") will skip the current test and @unittest.expectedFailure will expect the test to fail. If the tests passes it will report a failure.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from unittest import TestCase, expectedFailure, skip

class TestUser(TestCase):
    """User Test Case"""

    @skip("Example of skip")
    def test_user_can_login(self):
        """Test that the user can login."""
        self.assertTrue(True)

    @expectedFailure
    def test_failure(self):
        """Example of test failure."""
        self.assertEqual(1, 2)

And the text runner will show us that we skipped one test and expected one failure successfully.

1
2
3
4
5
6
7
8
9
10
11
$> py -m tests

test_failure (test_user.TestUser)
Example of test failure. ... expected failure
test_user_can_login (test_user.TestUser)
Test that the user can login. ... skipped 'Example of skip'

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK (skipped=1, expected failures=1)

There are times where mutiple assertions are written in a test. In those cases we can make the assertions appear as different tests on the runner output using the subTest() function.

1
2
3
4
5
6
7
8
9
10
11
12
from unittest import TestCase, expectedFailure, skip

class TestUser(TestCase):
    """User Test Case"""

    def test_user_can_login(self):
        """Test that the user can login."""
        self.assertTrue(True)

        for i in range(0, 3):
            with self.subTest(i=i):
                self.assertEqual(i, 1)

This will allow us to have a separated error showed for every subtest:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$> py -m tests

test_user_can_login (test_user.TestUser)
Test that the user can login. ...
======================================================================
FAIL: test_user_can_login (test_user.TestUser) (i=0)
Test that the user can login.
----------------------------------------------------------------------
Traceback (most recent call last):
  File, line 12, in test_user_can_login
    self.assertEqual(i, 1)
AssertionError: 0 != 1

======================================================================
FAIL: test_user_can_login (test_user.TestUser) (i=2)
Test that the user can login.
----------------------------------------------------------------------
Traceback (most recent call last):
  File, line 12, in test_user_can_login
    self.assertEqual(i, 1)
AssertionError: 2 != 1

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=2)

We can see that the test reported the error for the separate value used 0 and 2.

Assertions

unittest comes packed with a set of assertions. Here’s a list of the common assertions:

  • assertEqual
  • assertTrue
  • assertRaises
  • assertIs
  • assertIn
  • assertNone
  • assertIsInstance

Their negation counter part also exists, for example assertNotEqual or assertFalse.

There are also assertions used to check exceptions warnings and log messages:

  • assertRaises
  • assertRaisesRegex
  • assertWarns
  • assertWarnsRegex
  • assertLogs

And lastly assertion checking specific comparisons:

  • assertAlmostEqual
  • assertGreater
  • assertGreaterEqual
  • assertLess
  • assertLessEqual
  • assertRegex
  • assertCountEqual

The full list with the signature of each assertion is available on the official documentation. And that concludes today’s post!

Conclusion

Today we looked into unittest, a module included in the Python core allowing us to perform unit tests. We started by looking at how we could set it up then we looked at how writing test cases looked like and we completed this post by looking at the build in assertions available. I hope youike this post and I see you on the next one!

External Sources

Designed, built and maintained by Kimserey Lam.