8/21/2014

Inner product, round 2: unit tests and basic exception handling

Entering round 2, our current sub-problem backlog looks like this:

P1. Prompting the user
P2. Computing the inner product. 
P3. Displaying the output.
P4. Test inner product computation in a separate project. 

Let's pick P4 for this round. Here, you will learn how to do unit testing and exception handling.

Step U. Understanding the problem.

To the whole program, the function computeInnerProduct is called a unit. By unit testing the function computeInnerProduct, we mean exercising it in numerous ways, away from other units of the program. So far we have two tests, one for normal computation and the other for dimension error, all inside the main program. So, our main program is a unit testing suite consisting of two test cases. How do we know the test results are as expected? We checked the console output. Checks like these can be tedious and error-prone, especially when you have many tests. (And If you are serious about unit testing, you will have many tests.) It is easy for you to miss a message indicating test failure.

There is another subtle but profound problem with unit testing inside the main function. When there are many tests, it becomes tempting to reuse some of the variables used in the tests. When this happens, the reused variables may have already acquired values from previous tests. They may no loner hold the correct values for checking the execution results. In other words, the variables are not clean. This can make test results difficult to decipher and trust

Obviously, you can do better than writing test cases in a main function. Before introducing you to one such way, let's summarize what is needed. A unit test is usually composed of a number of test cases, each of which is executed in four steps:

1. Prepare test data.
2. Call the function tested and obtain result.
3. Compare result with expected result.
4. Clean up the test data.
Such repetitive execution makes unit tests a suitable target for framework support. Many unit testing frameworks are available. In these articles, we will be using CppUnitLite, a dialect of xUnit for testing C++ programs. 

In addition to doing most of the repetitive work for you, a unit testing framework like CppUnitLite also makes it easy for you to organized test cases. When combined with the strategy of separating test projects from a production project, this gives you a nice way to accumulate many unit tests in well-organized projects.

For handling the case when two vectors of different dimensions are passed to the function computeInnerProduct, C++ provides a much better way: exception handling
 
Step D. Devising a plan.

Let's build our task backlog for sub-problem P4 as follows:

T1. Prepare the CppUnitLite library.
T2. Set up a test project and practice writing unit tests.
T3. Change the function computeInnerProduct so that it can be tested in the test project. 
T4. Relocate test cases from main to the test project.
T5. Learn exception, exception detection and exception handling. 
T6. Replace forced program exit with exception handling. 

The task T3 prepares the function computeInnerProduct for unit testing. The code of the function is allocated in a header file and an implementation file. (T3)
 
Step C. Carrying out the plan.

T3. Change the function computeInnerProduct so that it can be tested in the test project.
We need to make the function computeInnerProduct accessible to the unit tests. Wit unit tests to reside in a separate file, this is done by separating a function into a declaration and a definition; the former is allocated to a header (.h) file and the latter in an implementation (.cpp) file.
   
T6. Replace forced program exit with exception handling.  
By throwing a C-style string, we now replace the forced program exit with following code:

double computeInnerProduct (double v1[], double v2[], int d1, int d2)
{
    if (d1 != d2)
        throw "Vectors of different dimension!";

    double r=0;
    for (int i=0; i<d1; ++i) {
        r += v1[i]*v2[i];
    }
    return r;
}


To test it, enclose the call to the function comuteInnerProduct in a try block and the check in a catch clause:

TEST (computeInnerProduct, dimension_error)
{
    double u[2] ={1,0};
    double v[3] ={1,1,1};

    try {
        computeInnerProduct(u,v,2,3);
    }
    catch (const char * s) {
        CHECK(0==strcmp("Vectors of different dimension!",s));
    

    }
}

Here's the working program.

Step L. Looking back.

We now have a separated test project for testing computeInnerProduct. The test project is now the working program for sub-program P2. Can you see the difference? Rather than showing a screen full of messages that confuse you, CppUnitLite is silent if all tests pass, as below:



Best of all, CppUnitLite is specific when a test fails:
 
Another important thing we have learned is writing unit tests for the exception handling code. (T6)

In the future, when we solve sub-problems P1 and P3, we shall write unit tests for any functions we code up. When any change is made to the functions in the production project, make sure to run the unit tests in the test project. This way, if any thing is broken, we'll be able to know quickly.

Now that we have freed up the main program from borrowed use by the tests, we can move forward to the next sub-problems.

© Y C Cheng, 2013, 2014. All rights reserved.

No comments:

Post a Comment