Unit Testing in Python - Unit testing framework

Odebode OpeyemiOdebode Opeyemi
7 min read

What is Testing?

Testing is an important phase in software development not just in python. Unit testing is a smaller test, it checks a single component that it is working in right way or not. Using the unit test, we can separate what necessities to be fixed in our system. Individual components like functions, modules or class of a software program or application are tested during unit testing.

The primary goal of unit testing is to guarantee that each component runs smoothly. Generally, software developers themselves undertake unit testing.

Why do you need unittest as a software developer?

Code that is not tested can’t be trusted

  • Unit tests assist to fix errors early in software development, which reduces costs.
  • It aids developers in comprehending the testing software components and facilitates changes rapidly.
  • Unit tests that are well-written serve as development documentation.
  • Unit tests aids in writing a better code
  • The overall structure will only function effectively if all of its components work properly.

How to write unit test in python using unittest module

let's create a simple function that add two number and name it sum.py

sum.py

def (a, b):
  return a + b

Unit test cases should be performed separately so that if modifications are needed, they may be changed without affecting the others.

Each of our python file or module should have its own test file that we can run test with, This test file never actually run in production, so it's only for development.

Now, let's create sum_test.py file and follow the steps below:

sum_test.py

import unittest
from sum import Add

class test_sum(unittest.TestCase):
    def test_normal_case(self):
        result = Add(2, 5)
        self.assertEqual(result, 7)

unittest.main()

from the code above code,

  • we Import python unittest built in module
  • we Import Add function from sum.py
  • The way unittest works we create a class and name it whatever we want, in our case we named it test_sum , then we inherit what unittest gives us which is unittest.Testcase
  • We create a method inside our class, named test_normal_case.

NOTE: The individual tests are defined with methods whose names start with the letters test. This naming convention informs the test runner about which methods represent tests.

  • Now, we can write our test for normal case inside test_normal_case method

  • we call the Add function and store the return value in result variable. Then we call self.assertEqual(result, 7) to check if result == 7. If the values do not compare equal, the test will fail.

here is a lists of the most commonly used assert methods

  • unittest.main() run the entire test file within test_sum class

by running our code from the terminal we got

$ python3 sum_test.py
.
----------------------------------------------------------------------
Ran 1 test in 0.016s

OK

That's cool right? Ok, let's call the self.assertEqual(result, 12) i. e with wrong answer and check out the error...

F
======================================================================
FAIL: test_normal_case (__main__.test_sum)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "sum_test.py", line 7, in test_normal_case
    self.assertEqual(result, 12)
AssertionError: 7 != 12

----------------------------------------------------------------------
Ran 1 test in 0.015s

FAILED (failures=1)

From the output above it is clear that we have an AssertionError which means that the differences between what came out of the function and what we were expecting through this test are different.

Now let's create more test case method by updating our sum_test file

sum_test.py

import unittest
from sum import Add

class test_sum(unittest.TestCase):
    def test_normal_case(self):
        result = Add(2, 5)
        self.assertEqual(result, 7)

    def test_string_case(self):
        result = Add("6", 6)
        self.assertEqual(result, 12)

unittest.main()

The output is:

$ python3 sum_test.py
.E
======================================================================
ERROR: test_string_case (__main__.test_sum)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "sum_test.py", line 10, in test_string_case
    result = Add("6", 6)
  File "/sum.py", line 2, in Add
    return a + b
TypeError: can only concatenate str (not "int") to str

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

FAILED (errors=1)

From the output above, we have TypeError because we received string input instead of integer. Hence, we can improve sum.py file to make sure we only perform operation on integer input.

sum.py

def Add(a, b):
    return int(a) + int(b)

From the code above we cast the input data to integers before performing operation, And our test will run without error.

Output

$ python3 sum_test.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.004s

OK

More tests with assertIsInstance() method

sum_test.py

import unittest
from sum import Add

class test_sum(unittest.TestCase):
    def test_normal_case(self):
        result = Add(2, 5)
        self.assertEqual(result, 7)

    def test_string_number(self):
        result = Add("6", 6)
        self.assertEqual(result, 12)

    def test_string_char(self):
        result = Add("Python", 6)
        self.assertIsInstance(result, ValueError)

unittest.main()

in test_string_char method, we passed string character as input data but this will result in ValueError because python can not cast string characters to integer, Then we checked if result variable is an instance of ValueError. So we have to catch the error in our sum.py to return instance of the ValueError

sum.py

def Add(a, b):
    try:
        return int(a) + int(b)
    except ValueError as err:
        return err

Output

$ python3 sum_test.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

That's great, all our tests passed...

Running multiple test in unison

Let's run our sum_test.py files only if it is a main file, Add this code snippet to sum_test.py

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

After that we will rename our sum_test.py to test_sum.py because this naming convention informs the test runner about which module/file represent tests. so that we can call our test file from the command line as module with python3 -m unittest.

You might be thinking, why do I need to run a test file as a module. it is useful because if we have more than one test file/modules and we wanna run all this test files as unison with python3 -m unittest command, this will run all the tests file at once.

Ok, let's check it out. Our test_sum.py should looks like this

import unittest
from sum import Add

class test_sum(unittest.TestCase):
    def test_normal_case(self):
        result = Add(2, 5)
        self.assertEqual(result, 7)

    def test_string_number(self):
        result = Add("6", 6)
        self.assertEqual(result, 12)

    def test_string_char(self):
        result = Add("Python", 6)
        self.assertIsInstance(result, ValueError)


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

let us duplicate test_sum.py and name it test_sum2.py to see the work of python3 -m unittest

test_sum2.py

import unittest
from sum import Add

class test_sum(unittest.TestCase):
    def test_normal_case(self):
        result = Add(2, 5)
        self.assertEqual(result, 7)

    def test_string_number(self):
        result = Add("6", 6)
        self.assertEqual(result, 12)

    def test_string_char(self):
        result = Add("Python", 6)
        self.assertIsInstance(result, ValueError)


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

Now, we have 3 tests from test_sum.py and 3 tests from test_sum2.py, so we have 6 tests all together.

Output using python3 -m unittest command

$ python3 -m unittest
......
----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK

Good!

setUp() and tearDown() method

Tests can be numerous, and their set-up can be repetitive. Luckily, we can factor out set-up code by implementing a method called setUp(), which the testing framework will automatically call for every single test we run:

The setUp() and tearDown() methods allow you to define instructions that will be executed before and after each test method.

NOTE: You can run tests with more detail (higher verbosity) by passing in the -v flag: python3 -m unittest -v

see example in test_sum2.py file below

test_sum2.py

import unittest
from sum import Add

class test_sum(unittest.TestCase):
    def setUp(self):
        print('\n********************************')
        print('about to run a test method')

    def test_normal_case(self):
        result = Add(2, 5)
        self.assertEqual(result, 7)

    def test_string_number(self):
        result = Add("6", 6)
        self.assertEqual(result, 12)

    def test_string_char(self):
        result = Add("Python", 6)
        self.assertIsInstance(result, ValueError)

    def tearDown(self):
        print('Done with test method!')


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

Output

$ python3 -m unittest -v
test_normal_case (test_sum.test_sum) ... ok
test_string_char (test_sum.test_sum) ... ok
test_string_number (test_sum.test_sum) ... ok
test_normal_case (test_sum2.test_sum) ... 
********************************
about to run a test method
Done with test method!
ok
test_string_char (test_sum2.test_sum) ... 
********************************
about to run a test method
Done with test method!
ok
test_string_number (test_sum2.test_sum) ... 
********************************
about to run a test method
Done with test method!
ok

----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK

Now, you see that the setUp() and tearDown() methods are called before and after each test method respectively on test_sum2.py file, and -v flag gives us more details about our tests

Enjoy!!!

If you love this article kindly follow me on twitter and linkedin

0
Subscribe to my newsletter

Read articles from Odebode Opeyemi directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Odebode Opeyemi
Odebode Opeyemi

Software Engineer I Technical Writer