NB: Unit Testing with Unittest

Unit Testing

Unit testing is a testing technique in which individual modules are tested to determine if there are any issues by the developer himself.

It is concerned with functional correctness of the stand-alone modules.

The main aim is to isolate each unit of the system to identify, analyze and fix the defects.

These units are typicallly functions and methods.

Benefits of Unit Testing

Developers can work in a predictable way on developing code.

Developers can write their own unit tests.

You can get a rapid response for testing small changes

Also:

Reduces defects in the newly developed features or reduces bugs when changing the existing functionality.

Reduces cost of testing, since defects are captured in very early phase.

Improves design and allows better refactoring of code.

Testing in Python is a huge topic and can come with a lot of complexity, but it doesn’t need to be hard. You can get started creating simple tests for your application in a few easy steps and then build on it from there.

The unittest Framework

One of the popular unit testing frameworks is Unittest. It is works well and is easy to use.

There are other tools, though – here’s a comparison of 6 Python testing frameworks.

The Basic Idea

The Unittest framework provides you with a bunch of assert methods, which are essentially wrappers around Python’s built-in assert function.

The basic idea is to write functions that test other functions by using these assert methods instead of peppering your code with them.

Unittest provides many assert methods – see this cheat sheet for more.

We will focus on three: * assertTrue() * assertFalse() * assertEqual()

The Basic Pattern

The Unittest framework works as follows:

Choose on a method or class that you want to test.

Create a class that is a subclass of unittest.TestCase.

In that class write methods that are designed to test the behavior of methods in the code you want to test.

  • These test methods focus on one behavior of one method (or function).
  • There can be many test methods for each target method.
  • Each test method name must be prefixed by test_.
  • Tests are executing in alphabetical order, so name them in the order you want them executed.
  • Each test makes use of an assert method. These methods typically compare expected with actual methods and return False if they don’t match and True if they do.
  • You always want tests to pass, so if you want to test if something breaks, you return True for a False condition.

Run the script and see the results.

Update the script as you create new methods or refactor existing ones.

Assert Methods

.assertTrue()

Negative Test Case

Run M08-02-script1.py

class TestStringMethods(unittest.TestCase):

    # test function
    def test_negative(self):

        testValue = False
        
        # error message in case if test case got failed
        message = "Test value is not true."
        
        # assertTrue() to check true of test value
        self.assertTrue(testValue, message)

if __name__ == '__main__':
    unittest.main()
!python M08-02-script1.py
test_negative (__main__.TestStringMethods.test_negative) ... FAIL

======================================================================
FAIL: test_negative (__main__.TestStringMethods.test_negative)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/rca2t1/Dropbox/Courses/DS/DS5100/DS5100-2023-07-R/repo/notebooks/M08_PythonTesting/M08-02-script1.py", line 14, in test_negative
    self.assertTrue(testValue, message)
AssertionError: False is not true : Test value is not true.

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

FAILED (failures=1)

Positive Test Case

import unittest

class TestStringMethods(unittest.TestCase):
    
    # test function
    def test_positive(self):
        
        testValue = True
        
        # error message in case if test case got failed
        message = "Test value is not true."
        
        # assertTrue() to check true of test value
        self.assertTrue( testValue, message)

if __name__ == '__main__':
    unittest.main()
!python M08-02-script2.py
test_positive (__main__.TestStringMethods.test_positive) ... ok

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

OK

.assertFalse()

Negative Test Case

import unittest

class TestStringMethods(unittest.TestCase):
    # test function
    def test_negative(self):
        testValue = True
        # error message in case if test case got failed
        message = "Test value is not false."
        # assetFalse() to check test value as false
        self.assertFalse( testValue, message)

if __name__ == '__main__':
    unittest.main()
!python M08-02-script3.py
F
======================================================================
FAIL: test_negative (__main__.TestStringMethods.test_negative)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/rca2t1/Dropbox/Courses/DS/DS5100/DS5100-2023-07-R/repo/notebooks/M08_PythonTesting/M08-02-script3.py", line 10, in test_negative
    self.assertFalse( testValue, message)
AssertionError: True is not false : Test value is not false.

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

FAILED (failures=1)

Positive Test Case

## unit test case
import unittest

class TestStringMethods(unittest.TestCase):
    # test function
    def test_positive(self):
        testValue = False
        # error message in case if test case got failed
        message = "Test value is not false."
        # assertFalse() to check test value as false
        self.assertFalse( testValue, message)

if __name__ == '__main__':
    unittest.main()
!python M08-02-script4.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

.assertEqual()

Here is a case where we expect two values to be equal.

Negative Test Case

## unit test case
import unittest

class TestStringMethods(unittest.TestCase):
    # test function to test equality of two value
    def test_negative(self):
        firstValue = "geeks"
        secondValue = "gfg"
        # error message in case if test case got failed
        message = "First value and second value are not equal !"
        # assertEqual() to check equality of first & second value
        self.assertEqual(firstValue, secondValue, message)

if __name__ == '__main__':
    unittest.main()
!python M08-02-script5.py
F
======================================================================
FAIL: test_negative (__main__.TestStringMethods.test_negative)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/rca2t1/Dropbox/Courses/DS/DS5100/DS5100-2023-07-R/repo/notebooks/M08_PythonTesting/M08-02-script5.py", line 12, in test_negative
    self.assertEqual(firstValue, secondValue, message)
AssertionError: 'geeks' != 'gfg'
- geeks
+ gfg
 : First value and second value are not equal !

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

FAILED (failures=1)

Positive Test Case

## unit test case
import unittest

class TestStringMethods(unittest.TestCase):
    # test function to test equality of two value
    def test_positive(self):
        firstValue = "geeks"
        secondValue = "geeks"
        # error message in case if test case got failed
        message = "First value and second value are not equal !"
        # assertEqual() to check equality of first & second value
        self.assertEqual(firstValue, secondValue, message)

if __name__ == '__main__':
    unittest.main(verbosity=2)
!python M08-02-script6.py
test_positive (__main__.TestStringMethods.test_positive) ... ok

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

OK

Example with User-defined Function

Function to test

def add_fish_to_aquarium(fish_list):
    if len(fish_list) > 10:
        raise ValueError("A maximum of 10 fish can be added to the aquarium")
    return {"tank_a": fish_list}

import unittest

Class to test the function

class TestAddFishToAquarium(unittest.TestCase):
    
    def test_add_fish_to_aquarium_success(self):
        actual = add_fish_to_aquarium(fish_list=["shark", "tuna"])
        expected = {"tank_a": ["shark", "tuna"]}
        self.assertEqual(actual, expected)

    def test_add_fish_to_aquarium_exception(self):
        too_many_fish = ["shark"] * 25
        with self.assertRaises(ValueError) as exception_context:
            add_fish_to_aquarium(fish_list=too_many_fish)
        self.assertEqual(
            str(exception_context.exception),
            "A maximum of 10 fish can be added to the aquarium"
        )

if __name__ == '__main__':
    unittest.main(verbosity=2)
!python M08-02-script7.py
test_add_fish_to_aquarium_exception (__main__.TestAddFishToAquarium.test_add_fish_to_aquarium_exception) ... ok
test_add_fish_to_aquarium_success (__main__.TestAddFishToAquarium.test_add_fish_to_aquarium_success) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

Example with External Class

We create a class called Student and save it in a local file called student.py.

class Student:
    
    # constructor
    def __init__(self, name, courses=None):
        self.name = name # string type
        self.courses = [] if courses is None else courses # list of strings
        self.num_courses = len(self.courses)
        
    # enroll in a course
    def enroll_in_course(self, course_name): 
        self.courses.append(course_name)
        self.num_courses += 1 # increment the number of courses

Then we create a companion test file for our class, saving it in a file called student_test.py.

from student import Student
import unittest

class EnrollInTestCase(unittest.TestCase): 
    
    def test_is_incremented_correctly(self):
        # test if enrollInCourse() method successfully increments the
        # num_courses attribute of the Student object 

        # Create student instance, adding some courses
        student1 = Student('Katherine', ['DS 5100'])
        student1.enroll_in_course("CS 5050")
        student1.enroll_in_course("CS 5777")
        print(student1.courses)
        print(student1.num_courses)
        
        # Test
        expected = 3
        # unittest.TestCase brings in the assertEqual() method
        self.assertEqual(student1.num_courses, expected)
        
if __name__ == '__main__':
    unittest.main(verbosity=2)
!python student_test.py
test_incremented_correctly (__main__.EnrollInCourseTest.test_incremented_correctly)
Test if enroll_in_course() method successfully increments the ... ['DS 5100', 'CS 5050', 'CS 5777']
3
ok

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

OK

The messages that unittest prints are error messages on Unix, so if we want to direct them to a file, we need to use 2>.

Notice how this command only shows the print messages contained in the program.

!python student_test.py 2> student_results.txt
['DS 5100', 'CS 5050', 'CS 5777']
3

This one, on the other hand, captures the print methods and only shows the errors.

!python student_test.py > student_results1.txt
test_incremented_correctly (__main__.EnrollInCourseTest.test_incremented_correctly)
Test if enroll_in_course() method successfully increments the ... ok

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

OK

Further Reading