Thursday, 19 July 2012

Unit Testing With Python

     While browsing through python and discovering new facets of python everyday, I found about unit testing feature in python. As the name suggests "Unit testing" is basically testing of code which are part of a larger module. It is nothing but a sort of white box testing where we are only bothered about whether the code gives a correct output rather than the more complex case that is how it was implemented. The advantages of using unit testing techniques is that we are able to identify the errors more easily, the fixing is simpler and most important it is less complex.

     Now when i talk about python and how it supports unit testing, there are 3 techniques that i came across. I would like to give you a gist of these techniques how you can incorporate the same while working on  python projects.

The 3 techniques are- Unit test, doctest, nose test

Why Unit test?

     Unit test is a module available for all python 2.1 and above. By importing this module in your program. The basic idea is to think of all the positive and negative inputs your program might face while its use. This way you start with all such scenarios even before actual building of code. An example for this I came across while browsing was with roman numerals. Roman numerals are meant only for counting purposes so only real life numbers are interpreted in roman numerals. So all negatives, fractions, zero find no place in roman numerals. Also they are limited to the range of 1--3999. So again anything beyond 3999 is a myth in roman. 

Now when we import unittest module we define the situations which the code will face in its test cases. We define a dictionary for every number till 3999.
  •  Test for success- We then decide positive test cases where the program should run successfully and reach the desired outcome. Eg: For any number in the range of 1-3999.
  • Test for failure- We then decide negative test cases where the program should throw an error as it doesn't satisfy the critiera. Eg: A number greater than 3999 or a decimal number, 0 etc. 
  • Test for sanity- We test if we convert a number to roman numeral then on converting back we should obtain the number itself and not lose it to roundoffs etc. Eg: tonumber(toroman(n)) = n should give n itself.

Why DocTest?

     Doctest is an interactive python to check whether the output given is according to the desired output or not. The ways to use doctest are:-
  • To check that a module’s docstrings are up-to-date by verifying that all interactive examples still work as documented.
  • To perform regression testing by verifying that interactive examples from a test file or a test object work as expected.
  • To write tutorial documentation for a package, liberally illustrated with input-output examples. Depending on whether the examples or the expository text are emphasized, this has the flavor of “literate testing” or “executable documentation”
     To explain it using the following example. We check the cases in which factorial conditions are to be checked. The last statement of the code imports doctest in which testmode() is used. This automatically provides testcases to the code while running it and checks for errors if any. To do so we run it using the command-- python filename.py -v. This returns a log file which shows the test cases given as input and returns ok if correct. It returns an error if it doesn't satisfy the condition.

def factorial(n:) 
  import math
    if not n >= 0:
        raise ValueError("n must be >= 0")
    if math.floor(n) != n:
        raise ValueError("n must be exact integer")
    if n+1 == n:  # catch a value like 1e300
        raise OverflowError("n too large")
    result = 1
    factor = 2
    while factor <= n:
        result *= factor
        factor += 1
    return result


if __name__ == "__main__":
    import doctest
    doctest.testmod()
 
     Now a common error, many of us may get an error that doctest doesn't have the file testmode in it. It is because we created a py file doctest.py which on running creates a file doctest.pyc. Remove the .pyc file, rename your file and try again, your file will run absolutely fine.

     Another feature is testfile(). Here it searches in any given .txt file, if it contains any python code in it and runs test cases for it and returns error message if any errors, or checks if its correct.
syntax- 

import doctest
doctest.testfile("example.txt")

Doctest also provides many options  which you can find in the doctest module like ignoring blankspaces, ellipses(...),dont accept true value flags, normalize whitespaces.
Doctest integrates tester class which has testsuites for integrating different testcases for different modules for it BASIC API. 
The advanced API has a framework as given in the figure.

                            list of:
+------+                   +---------+
|module| --DocTestFinder-> | DocTest | --DocTestRunner-> results
+------+    |        ^     +---------+     |       ^    (printed)
            |        |     | Example |     |       |
            v        |     |   ...   |     v       |
           DocTestParser   | Example |   OutputChecker
                           +---------+

It also provides objects like finder objects, parser objects, runner objects and output checker objects. It also provides debugger options which can be run under the python debugger pdb.

Why nose tool?

For nose tools testing you need to have nose installed in your systems, which can be easily done using apt - get install. Writing testcases is easier as nose collects cases from unittest.TestCase.
     Running testcases is easier as nose automatically collects as long as you follow some simple guidelines for organizing your library and test code. There’s no need to manually collect test cases into test suites. Running tests is responsive, since nose begins running tests as soon as the first test module is loaded. Nose supports fixtures at the package, module, class, and test case level, so expensive initialization can be done as infrequently as possible. It comes with builtin plugins to help in easier output capture, error finding, code coverage etc. 
The code shown below is an example code I tried for a python game. It gives a unit test skeleton for nose tools.

from nose.tools import *
from ex47.game import Room


def test_room():
    gold = Room("GoldRoom", 
                """This room has gold in it you can grab. There's a
                door to the north.""")
    assert_equal(gold.name, "GoldRoom")
    assert_equal(gold.paths, {})

def test_room_paths():
    center = Room("Center", "Test room in the center.")
    north = Room("North", "Test room in the north.")
    south = Room("South", "Test room in the south.")

    center.add_paths({'north': north, 'south': south})
    assert_equal(center.go('north'), north)
    assert_equal(center.go('south'), south)
 
On simply running nosetests on your terminal, you get the following output log:-

~/projects/simplegame $ nosetests
...
----------------------------------------------------------------------
Ran 3 tests in 0.007s

OK
 
  
Now that we have more than one way to test our code with little more ease, all we need to do now is start right away thinking of test cases for a code still in pipeline. All I can say for now is HAPPY TESTING to all!!!  

No comments:

Post a Comment