© Paul Gerrard 2016
Paul GerrardLean Python10.1007/978-1-4842-2385-7_8

8. Testing Your Code

Paul Gerrard
(1)
Maidenhead, Berkshire, UK
 

Modularizing Code and Testing It1

So far, we have explained how to make use of the features of Python to create software that has some purpose and hopefully, value. When you write a little code, the natural thing to do then is to try it out, or test it.
The programs I have used to illustrate Python features run without any intervention, or require some user input via the input() function. As you get better at programming, you will become more ambitious and create larger programs. Then you realize that testing becomes more difficult, and more important. Splitting programs into functions and modules will make testing and debugging easier, as I said earlier.

Test-Driven Development

As your programs get bigger and more complicated, the chances of making a mistake or making a change that has an unwanted side effect increase. Should we run all of our previous tests every time we make a change then? It would be helpful, but doesn’t the thought of running the same tests again and again bore you? Of course it might.
The test-driven development (TDD) approach for programming is gaining popularity. This is how it works:
  • Developers write their (automated) tests first, before they write code.
  • They run their tests and watch them fail, then add or correct code to make them pass.
  • When their tests pass, they look for opportunities to improve the design of their code. Can you think of why?
TDD might not always be the best approach, but when it comes to writing larger programs it is best to modularize your code. When it comes to writing and testing classes and functions, using a unit test framework to create automated tests makes a lot of sense.

The unittest Framework

In this chapter, I provide a brief introduction of how the unittest framework [20] can be used to test Python modules.
Suppose we need to write a function that performs simple arithmetic. The function is passed two numbers and an operator, which could be any of '+', '-', '*', or '/' to simulate addition, subtraction, multiplication, or division. It is as simple as that. Our function call might look like this:
result, msg = calc(12.34, '*', 98.76)
result would be the outcome of the calculation, or None if an error occurred. msg would contain a string version of the result or an error message if the calculation fails for some reason.
The line of code that called the calc function in this example looks like a test doesn’t it? It is, except we haven’t checked that the outputs (result and msg) are correct. In this case we would expect that:
  • result would have the value 1218.6984.
  • msg would have the value '1218.6984'.
Here is a possible implementation of the calc function in file calc.py:
def calc(a, op, b):
    if op not in '+-/*':
        return None, 'Operator must be +-/*'
    try:
        if op=='+':
            result=a+b
        elif op=='-':
            result=a-b
        elif op=='/':
            result=a/b
        else:
            result=a*b
    except Exception as e:
        return None,e.__class__.__name__
    return result,str(result)
The calc function does very little checking. It does no checking of the numeric values of the two number arguments a and b. After that, it attempts the calculation but traps any exceptions that occur, passing the name of the exception back in msg.
Now, to create a set of tests for the calc function, we have created a testcalc.py file as follows:
1   import unittest
2   import calc
3   #
4   #   define the test class
5   #
6   class testCalc(unittest.TestCase):
7  
8       def testSimpleAdd(self):
9           result,msg = calc.calc(1,'+',1)
10          self.assertEqual(result,2.0)
11  
12      def testLargeProduct(self):
13          result,msg = calc.calc(123456789.0, '*',987654321.0)
14          self.assertEqual(result, 1.2193263111263526e+17)
15      
16      def testDivByZero(self):
17          result,msg = calc.calc(6,'/',0.0)
18          self.assertEqual(msg,'ZeroDivisionError')
19  #
20  #   create the test suite
21  #
22  TestSuite = unittest.TestSuite()
23  #
24  #   add tests to the suite
25  #
26  TestSuite.addTest(testCalc("testSimpleAdd"))
27  TestSuite.addTest(testCalc("testLargeProduct"))
28  TestSuite.addTest(testCalc("testDivByZero"))
29  #
30  #   create the test runner
31  #
32  runner = unittest.TextTestRunner()
33  #
34  #   execute the tests
35  #
36  runner.run(TestSuite)
  • Line 1 imports the unittest module that we will use to test the calc function.
  • Line 2 imports the calc module (the module to be tested).
  • Line 6 defines the class (testCalc) that defines the tests.
  • Lines 8 through 18 define three tests. The format of each is similar.
    • Each test has a unique name (normally test...).
    • It calls the function to be tested in some way.
    • It performs an assertion to check correctness (we cover assertions further later).
  • Line 22 defines the test suite that will be run.
  • Lines 26 through 28 add tests to the test suite (note that we can create multiple test suites with different selections of test).
  • Line 32 defines the test runner.
  • Line 36 runs the tests.
When we run the test we get this:
D:LeanPythonprograms>python testcalc.py
...
----------------------------------------------------
Ran 3 tests in 0.001s
OK
The three dots appear as you run the tests, and represent the successful execution of each test. If a test had failed, we would have seen a Python error message indicating the exception and line number in the program where the failure occurred.
All this code just to run a few tests might seem a little excessive. Perhaps it looks a bit wordy, but there is purpose in each call. Note, however, that once it is set up, if I want to add a new tests, I create a new test call (e.g., lines 8–10) and add the test to the suite (e.g., line 26). Once you are set up, therefore, creating large numbers of tests is easy. The calc function is a rather simplistic example. More realistic (and complex) classes and functions sometimes require 20 or 30 or even hundreds of tests.
Note
Programmers who offer their modules as open-source libraries often include a large suite of tests with their modules.2

Assertions

The key to good testing is the choice of inputs or stimuli applied to your code (the function call; e.g., line 9) and the check that is performed on the outcome. The checks are implemented as assertions . The unittest module provides around 20 different assertion variations. Examples include the following:
  • assertEquals. This variation asserts exact equality between two values (the result and your predicted result).
  • assertTrue. Is an expression true?
  • assertIn. Is a value in a sequence?
  • assertGreaterEqual. Is one value greater than or equal to another?

More Complex Test Scenarios

The unittest framework has many more features. Examples are setup() and teardown() methods in the TestCase class. These methods are called automatically, just before and just after each test case, to perform a standard setup (of variables, data, or the environment) to allow each test to run correctly. The teardown process tidies up after the test (if necessary).
Note
We have now covered the basic elements of the Python language and explored the unittest module for testing. Now, let's look at using some popular libraries to do something useful.
Footnotes
1
Most programs are split into modules that are separately tested by the programmer. This testing is usually called unit or component testing.
 
2
If they don’t, perhaps their modules should be avoided.
 
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.82.154