Unit testing in Python

Unit Testing in Python Using unittest Module

The goal of this article is to provide a quick introduction to Python’s unit testing module called unittest. It is the essence, the very basic information you need to quickly start unit testing in Python.

Introduction to the unittest Module

Key points about unit testing in Python:

  • modules with tests should import unittest module,
  • tests should be defined inside a class extending the unittest.TestCase class,
  • every test should start with the test word,
  • if a test has a doc. comment (between a pair of ''', i. e. three apostrophes), the comment will be printed when the test is being run (if verbose mode was set),
  • tests can have setUp and tearDown methods – those methods will be called, respectively, before and after each of the tests; there are also class-level set up and tear down methods,
  • to execute the tests when the module is run, unittest.main() should be called,
  • to see which tests are called with additional info, the -v (verbose) parameter should be specified when the module with tests is executed.

Example:

# file test_is_odd.py
import unittest

def is_odd(param):
    return param % 2 == 1

class TestOdd(unittest.TestCase):
    def test_is_odd(self):
        ''' 1 should be classified as odd '''
        self.assertTrue(is_odd(1))

    def test_is_not_odd(self):
        ''' 2 should not be classified as odd '''
        self.assertFalse(is_odd(2))

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

Running the module:

python test_is_odd.py -v

Output:

test_is_not_odd (__main__.TestOdd)
2 should not be classified as odd ... ok
test_is_odd (__main__.TestOdd)
1 should be classified as odd ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

Running tests can be stopped after the first test fails using the -f argument:

# file test_fail_fast.py
import unittest

class TestMul(unittest.TestCase):
    def test_first(self):
        ''' First failing test '''
        self.assertTrue(False)

    def test_second(self):
        ''' Second failing test '''
        self.assertTrue(False)

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

Command:

python test_fail_fast.py -v -f

Output:

First failing test ... FAIL

======================================================================
FAIL: test_first (__main__.TestMul)
First failing test
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_fail_fast.py", line 7, in test_first
    self.assertTrue(False)
AssertionError: False is not true

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)

Success, Failure & Error

Tests can have one of three possible outcomes:

  • Success – no error occurred during the test and none of the assertions failed.
  • Failure – no error occurred during the test and one of the assertions failed.
  • Error – an error occurred during the execution of the test (for example, an unexpected exception was thrown).

Example:

# file test_s_f_e.py
import unittest

class TestSFE(unittest.TestCase):
    def test_first(self):
        ''' Test that passes '''
        self.assertTrue(True)

    def test_second(self):
        ''' Test that fails '''
        self.assertTrue(False)

    def test_third(self):
        ''' Test during which an error occurs '''
        raise Exception("Test error.")

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

Output:

test_first (__main__.TestSFE)
Test that passes ... ok
test_second (__main__.TestSFE)
Test that fails ... FAIL
test_third (__main__.TestSFE)
Test during which an error occurs ... ERROR

======================================================================
ERROR: test_third (__main__.TestSFE)
Test during which an error occurs
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_s_f_e.py", line 15, in test_third
    raise Exception("Test error.")
Exception: Test error.

======================================================================
FAIL: test_second (__main__.TestSFE)
Test that fails
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_s_f_e.py", line 11, in test_second
    self.assertTrue(False)
AssertionError: False is not true

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1, errors=1)

Fixtures

A fixture is a preparation and cleanup of the context and data for unit tests environment.

The preparation part is done by the setUp method, which, if present, is called before each of the tests. If it fails for any reason, the test will not be run.

The cleanup is done in the tearDown method. It is run regardless of whether the test succeeded or not.

There are also set up and tear down methods for the whole classes containing tests:

  • setUpClass – executed before all tests,
  • tearDownClass – executed after all tests.

Both must be defined with the @classmethod adnotation. Example:

# file test_setup.py
import unittest

def is_odd(param):
    return param % 2 == 1

class TestSetup(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        print("Setting up the class.")

    @classmethod
    def tearDownClass(cls):
        print("Tearing down the class.")

    def setUp(self):
        print("Setting up the test.")

    def tearDown(self):
        print("Tearing down the test.")

    def test_is_odd(self):
        ''' 1 should be classified as odd '''
        self.assertTrue(is_odd(1))

    def test_is_not_odd(self):
        ''' 2 should not be classified as odd '''
        self.assertFalse(is_odd(2))

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

Output:

Setting up the class.
test_is_not_odd (__main__.TestSetup)
2 should not be classified as odd ... Setting up the test.
Tearing down the test.
ok
test_is_odd (__main__.TestSetup)
1 should be classified as odd ... Setting up the test.
Tearing down the test.
ok
Tearing down the class.

----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

Test Discovery

Test discovery can be used to run all tests in a given directory. Modules’ file names should start with test. If there was another test file in the directory, for example:

# file test_mul.py
import unittest

def mul(param1, param2):
    return param1 * param2

class TestMul(unittest.TestCase):
    def test_mul_by_0(self):
        ''' 1 * 0 should be 0 '''
        self.assertEqual(mul(1, 0), 0)

    def test_square(self):
        ''' 2 * 2 should be 4 '''
        self.assertEqual(mul(2, 2), 4)

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

Tests from both files could be run using the following command (-v parameter for verbose mode):

python -m unittest discover -v

Output:

test_is_not_odd (test_is_odd.TestOdd)
2 should not be classified as odd ... ok
test_is_odd (test_is_odd.TestOdd)
1 should be classified as odd ... ok
test_mul_by_0 (test_mul.TestMul)
1 * 0 should be 0 ... ok
test_square (test_mul.TestMul)
2 * 2 should be 4 ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.002s

OK

Additional parameter can be specified when using the above command – starting location and a pattern to match files with tests, for example:

# current location: . (dot – current folder)
python -m unittest discover -v . „*.py”

Assertion methods

Most of the assert* methods accept an optional msg argument. The message it contains is printed when the test fails and may serve as a source of additional information about the failure.

Source of information about assertion methods: Python’s Documentation – unittest module.

Basic assertion methods

Method Checks if
assertEqual(a, b) a == b
assertNotEqual(a, b) a != b
assertTrue(x) bool(x) is True
assertFalse(x) bool(x) is False
assertIs(a, b) a is b
assertIsNot(a, b) a is not b
assertIsNone(x) x is None
assertIsNotNone(x) x is not None
assertIn(a, b) a in b
assertNotIn(a, b) a not in b
assertIsInstance(a, b) isinstance(a, b)
assertNotIsInstance(a, b) not isinstance(a, b)

Note: assertEqual automatically uses one of the assertion methods defined specifically for some of the types, for example, strings, lists, sets etc. – it is not required to call them directly.

Additional methods

There are additional assertion methods for checking, for example, if two floating-point numbers are equal when rounded to specified number of decimal places (7 is the default).

Method Checks if
assertAlmostEqual(a, b) round(a-b, 7) == 0
assertNotAlmostEqual(a, b) round(a-b, 7) != 0
assertGreater(a, b) a > b
assertGreaterEqual(a, b) a >= b
assertLess(a, b) a < b
assertLessEqual(a, b) a <= b
assertRegex(s, r) r.search(s)
assertNotRegex(s, r) not r.search(s)
assertCountEqual(a, b) a and b have the same elements in the same number, regardless of their order

Expecting exceptions

It is also possible to assert that an exception or a warning will be thrown. The *Regex versions allow to validate the message of the exception against the given regular expression.

Method Checks if
assertRaises(exc, fun, *args, **kwds) fun(*args, **kwds) raises exc
assertRaisesRegex(exc, r, fun, *args, **kwds) fun(*args, **kwds) raises exc
and the message matches regex r
assertWarns(warn, fun, *args, **kwds) fun(*args, **kwds) raises warn
assertWarnsRegex(warn, r, fun, *args, **kwds) fun(*args, **kwds) raises warn
and the message matches regex r

In the below example, we’re passing a character to check if it is an odd number. The first test expects the ValueError exception to be thrown. The second one also checks if the message of the exception is in the expected format:

# file test_assert.py
import unittest

def is_odd(param):
    try:
        return int(param) % 2 == 1
    except ValueError:
        raise ValueError("'{0}' is not a number!".format(param))

class TestRaises(unittest.TestCase):
    def test_raises_value_error(self):
        ''' "a" should cause a ValueError exception to be thrown '''
        self.assertRaises(ValueError, is_odd, "a")

    def test_raises_value_error_regex(self):
        ''' "a" should cause a ValueError exception to
                be thrown with the specific message '''
        self.assertRaisesRegex(ValueError, "'.*' is not a number!",
                               is_odd, "a")
if __name__ == '__main__':
    unittest.main()

Output:

test_raises_value_error (__main__.TestRaises)
"a" should cause a ValueError exception to be thrown ... ok
test_raises_value_error_regex (__main__.TestRaises)
"a" should cause a ValueError exception to ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

Further Reading & Useful Links

That was a quick look at the unit testing in Python. I’ve barely scratched the surface here. There are many other modules that help and enhance unit testing in Python.


Below you’ll find links to some useful resources on the Internet about topics covered in this article:


I hope you enjoyed my article. If you have found any errors in it (even typos), you think that I haven’t explained anything clearly enough or you have an idea how I could make the article better – please, do not hesitate to contact me, or leave a comment.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *