DEV Community

Edwin Torres
Edwin Torres

Posted on

Unit Testing in Python

Testing your code is just as important as writing it.

If the user can do it, the user will do it.

Our programs should be as robust as possible. In other words, a program must work correctly for all possible scenarios. This is why unit testing is so important.

Unit testing is the process of verifying and validating code 🔍. A unit test specifies the program input and expected result. You write unit test cases for all possible scenarios. For every input, the program must produce the correct result.

Of course it is not practical to test every possible input. But we can generalize our test cases over a range of possible values. Then we can have confidence that our programs work correctly.

Imagine that your task is to write a Python program to count the number of occurrences of a number, or key, in an array and return the count. Here is a program that defines a count() function to do that:

# countkey.py
def count(k,l):
  # count occurrences of k in l
  num = 0
  for i in l:
    if i == k:
      num += 1
  return num  # return the count
Enter fullscreen mode Exit fullscreen mode

Now consider this unit test, where the input values are the number to search for and the array to search, followed by the expected output:

  • Input: 1, [1,2,3,1]
  • Expected Output: 2

Since 1 appears in the array twice, the expected return value is 2.

Python provides a unit testing framework called unittest that automates unit test cases. Given the countkey.py program that we are testing, here is another program called tester.py that automates the unit test:

# tester.py
import countkey  # the program to test
import unittest  # Python test framework

class TesterClass(unittest.TestCase):
  # test cases

  def test1(self):
    self.assertEqual(countkey.count(1,[1,2,3,1]), 2)

if __name__ == '__main__':
  unittest.main()
Enter fullscreen mode Exit fullscreen mode

In the above program, there is one test case which is the test1() method. This method asserts that the count() function returns the value 2, when given the input parameters: 1 , [1,2,3,1]. When you execute the tester.py program, the output is:

$  python tester.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
$
Enter fullscreen mode Exit fullscreen mode

The test case passed. But there are a lot more scenarios to test if we want to ensure that the program is correct and robust. Identifying these scenarios is vital to the success of your programs. Here are some other unit tests:

  • Count occurrences of a positive number
  • Count occurrences of a negative number
  • Count occurrences when the key is at the beginning of the array
  • Count occurrences when the key is in the middle of the array
  • Count occurrences when the key is at the end of the array
  • Count occurrences when the key occurs multiple times
  • Count occurrences when the key does not occur at all
  • Count occurrences when the array is empty
  • Count occurrences when searching for the key None, and it is in the array
  • Count occurrences when searching for the key None, and it is not in the array
  • Count occurrences when searching in None instead of an array

These are not all possible unit tests, but they do check all types of scenarios such as different numbers, boundaries, empty arrays, etc.

Here is the complete tester.py program that automates all of the above unit tests:

# tester.py
import countkey  # the program to test
import unittest  # Python test framework

class TesterClass(unittest.TestCase):
  # test cases

  def test1(self):
    # find positive key
    self.assertEqual(countkey.count(6,[6,7,8]), 1)

  def test2(self):
    # find negative key
    self.assertEqual(countkey.count(-7,[6,-7,8]), 1)    

  def test3(self):
    # key is found at the beginning
    self.assertEqual(countkey.count(6,[6,7,8]), 1)

  def test4(self):
    # key is found in the middle
    self.assertEqual(countkey.count(7,[6,7,8]), 1)    

  def test5(self):
    # key is found at the end
    self.assertEqual(countkey.count(8,[6,7,8]), 1)        

  def test6(self):
    # multiple keys found
    self.assertEqual(countkey.count(1,[1,6,1,7,8,1]), 3)  

  def test7(self):
    # key is not found
    self.assertEqual(countkey.count(9,[1,2,3,1]), 0)

  def test8(self):
    # empty array
    self.assertEqual(countkey.count(3,[]), 0)

  def test9(self):
    # Find None when it is there
    self.assertEqual(countkey.count(None,[1,2,None,3]), 1)

  def test10(self):
    # Find None when it is not there
    self.assertEqual(countkey.count(None,[1,2,3]), 0)

  def test11(self):
    # Find in None?
    self.assertEqual(countkey.count(3,None), 0)

if __name__ == '__main__':
  unittest.main()
Enter fullscreen mode Exit fullscreen mode

When you execute tester.py, the output is:

$  python tester.py
..E........
======================================================================
ERROR: test11 (__main__.TesterClass)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "tester.py", line 51, in test11
    self.assertEqual(countkey.count(3,None), 0)
  File "/Users/user/Desktop/countkey.py", line 5, in count
    for i in l:
TypeError: 'NoneType' object is not iterable

----------------------------------------------------------------------
Ran 11 tests in 0.001s

FAILED (errors=1)
$
Enter fullscreen mode Exit fullscreen mode

The output indicates that test11() failed. That is because the program does not properly handle the case when the count() function tries to search None instead of an array. We can modify countkey.py to make it more robust:

# countkey.py
def count(k,l):
  # count occurrences of k in l

  # NEW
  if l == None:
    return 0

  num = 0
  for i in l:
    if i == k:
      num += 1
  return num  # return the count
Enter fullscreen mode Exit fullscreen mode

Here is the output now:

$  python tester.py
...........
----------------------------------------------------------------------
Ran 11 tests in 0.000s

OK
$
Enter fullscreen mode Exit fullscreen mode

Since all the unit test cases are successful, we can be confident that the program is correct. Another advantage of automating unit test cases is that we can easily re-test the countkey.py whenever someone modifies it. All you have to do is execute the tester.py program. That is why it is important to write good, comprehensive unit test cases for your programs. And remember to update your tester programs as needed.

Unit testing is an important part of program development. Once you identify the full set of unit test cases, automate them with a testing framework like Python unittest. This will improve your program and ensure that the program is correct, now and in the future.

Thanks for reading. 😃

Follow me on Twitter @realEdwinTorres for more programming tips and help.

Top comments (0)