Unit testing is a method through which the smallest testable units of source code are tested to determine that they behave as expected. The important benefit of writing unit tests is that it enables us to check for any unwanted side effects when we alter the code and correct them.

The unittest framework is a built-in unit testing framework in Python.

Some terminology:

  • test fixture: the preparation needed to perform one or more tests, and any associate cleanup actions
  • test case: individual unit of testing which checks for the correct response for a given input

I will be covering a minimal subset of unittest that will be enough to understand what unit testing is and how to go about writing test cases. To learn more, refer to the official docs.

A simple example:

import unittest

class Test2(unittest.TestCase):
    
    def test_simple(self):
        self.assertEqual(2+2,4)

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

Run the test using the following command

python -m unittest -v test_2.py

Output:

test_simple (test_2.Test) ... ok

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

OK

If the module name is not provided, by default all unit tests in the current project are executed. The -v argument is used to display verbose output.

A few notes:

  • The unittest module is imported
  • Testcases are created by subclassing unittest.TestCase
  • The function that contains the tests begins with test

The TestCase class provides methods to check for and report failures.

Some of the methods are listed below:

  • assertEqual(a,b)
  • assertNotEqual(a,b)
  • assertTrue(x)
  • assertFalse(x)
  • assertIs(a,b)
  • assertIn(a,b)

Another example of writing test cases:

import unittest

def division(x,y):
    """Return result of division"""
    try:
        return x/y
    except ZeroDivisionError:
        print("Division by zero")

class Testexceptions(unittest.TestCase):

    def test_basic(self):
        self.assertEqual(division(10,5), 2.0)
        self.assertEqual(division(10,-2), -5.0)

    def test_type_exceptions(self):
        with self.assertRaises(TypeError):
            division(10,'a')
    
    def test_zero_division(self):
        try:
            division(10,0)
        except ZeroDivisionError:
            self.fail("ZeroDivisionError")

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

Output:

test_basic (t.Testexceptions) ... ok
test_type_exceptions (t.Testexceptions) ... ok
test_zero_division (t.Testexceptions) ... Division by zero
ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

The above example introduces assertRaises which is used to verify that an exception is raised. There is no assertNotRaises, so one way to achieve that is by using try and except as shown in the test_zero_division method above.

There are cases where there is a lot of repetitive code shared by tests, such as connecting to databases or fetching data from a URL. To factor out common code, there are a few methods that we can implement:

  • setUp() is called before the execution of every test method
  • tearDown() is called after the execution of every test method
  • setUpClass() is called before tests in an individual class are run
  • tearDownClass() is called after the tests in a class are run
  • setUpModule() is called before code in a module is run
  • tearDownModule() is called after all tests are run inside the module

The following example demonstrates the order in which the methods are called.

tests.py

import unittest

def setUpModule():
    print("setUpModule() from tests.py")

def tearDownModule():
    print("tearDownModule() from tests.py")

def division(x,y):
    """Return result of division"""
    try:
        return x/y
    except ZeroDivisionError:
        print("Division by zero")

class Testexceptions(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        print("setUpClass() from Testexceptions class in tests.py")
    
    @classmethod
    def tearDownClass(cls):
        print("tearDownClass() from Testexceptions class in tests.py")

    def setUp(self):
        print("setUp() from Testexceptions in tests.py")
    def tearDown(self):
        print("tearDown() from Testexceptions in tests.py")
    
    def test_basic(self):
        self.assertEqual(division(10,5), 2.0)
        self.assertEqual(division(10,-2), -5.0)

    def test_type_exceptions(self):
        with self.assertRaises(TypeError):
            division(10,'a')
    
    def test_zero_division(self):
        try:
            division(10,0)
        except ZeroDivisionError:
            self.fail("ZeroDivisionError")

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

test_2.py

import unittest

def setUpModule():
    print("setUpModule() from test_2.py")

def tearDownModule():
    print("tearDownModule() from test_2.py")

class Test2(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        print("setUpClass() from Test2 class in test_2.py")

    
    def setUp(self):
        print("setUp() from Test2 in test_2.py")
    
    def test_simple(self):
        self.assertEqual(2+2,4)

    @unittest.skip("Skipping a test demo")
    def test_skip(self):
        self.fail("Never happens")

    def tearDown(self):
        print("tearDown() from Test2 in test_2.py")
    
    @classmethod
    def tearDownClass(cls):
        print("tearDownClass() from Test2 class in test_2.py")


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

Output:

setUpModule() from test_2.py
setUpClass() from Test2 class in test_2.py
test_simple (test_2.Test2) ... setUp() from Test2 in test_2.py
tearDown() from Test2 in test_2.py
ok
test_skip (test_2.Test2) ... skipped 'Skipping a test demo'
tearDownClass() from Test2 class in test_2.py
tearDownModule() from test_2.py
setUpModule() from tests.py
setUpClass() from Testexceptions class in tests.py
test_basic (tests.Testexceptions) ... setUp() from Testexceptions in tests.py
tearDown() from Testexceptions in tests.py
ok
test_type_exceptions (tests.Testexceptions) ... setUp() from Testexceptions in tests.py
tearDown() from Testexceptions in tests.py
ok
test_zero_division (tests.Testexceptions) ... setUp() from Testexceptions in tests.py
Division by zero
tearDown() from Testexceptions in tests.py
ok
tearDownClass() from Testexceptions class in tests.py
tearDownModule() from tests.py

----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK (skipped=1)

The unittest.skip(msg) decorator can be used to skip a test. This is useful when a test is known to fail and it needs to be fixed.

Source code for today’s plog is here and here.

Reference: official docs.