Friday, September 21, 2012

Introduction to Unit Testing Using Python Unittest

Unit Testing is an extremely powerful tool.  It directly helps to ensure the 3 aspects of good software: Verifiability, Maintainability, and Extensibility.  Unit testing is as much a process of software design as it is a tool.  There are many great tutorials on HOW to use python unittest, and of course python documentation is an excellent resource.  I am going to focus on WHY to test your software.  Below are a couple short unit testing examples using python's built in unittest package.


Verfiability

How does one verify that their program works?  With unit testing we can create specific functions or groups of functions to target our code.  For example:

def sum_numbers(num_one, num_two):
   """return an integer, the sum of two numbers"""
   return num_one + num_two

We can easily create a test for this using python's built in unittest module.  Tests are created by subclassing unittest.TestClass

from mymodule.functions import sum_numbers

class TestFunctions(unittest.TestClass): 

    def test_add_numbers_success(self):
        self.assertEqual(sum_numbers(2, 2), 4)


On python 2.7+ running python -m unittest discover in our package will automatically search for all test.py files and run the TestClasses.  There are a couple of things to note in the above example.  All test classes must subclass unittest.TestClass.  The focal point of any test is its assertions.  The assertions are what dictate whether a test passes or fails.  Although this is a contrived example it displays how easy it is to isolate our methods and control exactly what inputs they receive!  If we were to create one test method (or more) for every method in our project we would quickly grow a test suite.  When making ANY changes to our code it becomes trivial to run through every single function in our project and verify that nothing has broken!  Imagine if we had a web app and every time we added a feature we would have to run through EVERY possible page/action!? It could take a long time doing it manually.


Maintainability

Bugs are a part of software development.  It is extremely important to minimize bugs but when they do happen it is important to create fixes very quickly.  Because bugs will occur in code it is important to have a process set up that helps to isolate the bug so that it is easy to reproduce, easy to correct and easy to verify the bug has been fixed.  Unittesting helps to do all of these.  Suppose the sum_numbers function is at the heart of a website.  It gets all sorts of user input data, and occasionally some faulty data slips through. If a string is passed as one of the parameters it will result in a TypeError!!  It is trivial to isolate and reproduce this bug.  I guess we need a little thought about what should be returned if there is an invalid input.  For this example lets return None.  We then can create another test method like:


class TestFunctions(unittest.TestClass): 

    def test_add_numbers_success(self):
        self.assertEqual(sum_numbers(2, 2), 4)

    def test_add_numbers_string_bug(self):
        self.assertEqual(sum_numbers('a', 2), None)


Running the above code reproduces the string error.  Since we haven't fixed our code yet this test will fail.  We can then change our sum_numbers method to handle a type error:


def sum_numbers(num_one, num_two):
   """return an integer, the sum of two numbers, can't trust user input"""
   try:
     return int(num_one) + int(num_two)
   except ValueError:
     return None


Running our test again will result in two passing tests.  We successfully isolated the bug, reproduced the bug and verified the bug has been fixed!! We now also have a test trail assuring us the big has been addressed.  Pretty cool.


Extensibility

With tests it becomes very easy to help an app grow.  A test suite provides a safety net for an application.  We can programatically run through every function of an app in a short amount of time.  This could take hours to do manually.   Some test suites can take hours to run, it wouldn't even be feasible to manually test a large codebase!!   As long as we keep designing our apps in a modular unit based way we can easily add functions and tests for those individual functions.  Another aspect of unittesting is how easy it is to refactor code.  Suppose we had thought it was a good idea at the time to write our original function like:


def sum_numbers(num_one, num_two):
   """return an integer, the sum of two numbers"""
   return sum([num_one, num_two])


Assuming we had the same test method as before:


def test_add_numbers_success(self):
        self.assertEqual(sum_numbers(2, 2), 4)



This test is focused on the output of our function.  It is assuring us the output is as expected.   This allows us to easily change what is happening in our function and still have the test acting as a safety net.  We can rewrite (refactor) the internals of our methods and guarentee they are still functioning in the way we originally tested them!  Our method would pass the test because it is performing the action that we want it to.  We could change the method to remove the list and sum function


def sum_numbers(num_one, num_two):
   """return an integer, the sum of two numbers"""
   return num_one + num_two


We cleaned up our function and ensures that it functions in the way we designed it to!!


Testing is a powerful tool that should be very heavily considered.  It helps verify our functions are working the way we intended them, help us easily maintain our applications and help us extend our applications.  Correct aplication design will help us isolate our problems.  Testing can take a significant amount of time, but the benefits it offers far outweigh any downsides.

No comments:

Post a Comment