Unit Testing in Python - Unit testing framework
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 fromsum.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 isunittest.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
methodwe call the
Add
function and store the return value inresult
variable. Then we callself.assertEqual(result, 7)
to check ifresult == 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 withintest_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
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