Difference between revisions of "UnitTests"

From OPeNDAP Documentation
m (Header files)
m (CppUnit basic layout)
Line 21: Line 21:
 
</source>
 
</source>
  
...Also include the header that defines the class you want to test, other debug tools you use and whatever is needed for ''getopt'' in you build/target environment.
+
Then, include the header that defines the class you want to test, other debug tools you use and whatever is needed for ''getopt'' in you build/target environment. Also define static booleans for things like debug switches and other run-time options. Adding a ''debug'' option is good because these tests are often used to track down problems as well as being used as part of automated builds. A debug option lets people see what's wrong without gumming up the works when a machine runs the tests.
 
<source lang="cpp">
 
<source lang="cpp">
 
#include "AttrTable.h"    // The class to test
 
#include "AttrTable.h"    // The class to test

Revision as of 20:05, 6 May 2017

1 Writing Unit Tests for C++ using CppUnit

This page describes how to write unit tests for C++ classes using CppUnit. The basic process is fairly simple: a CppUnit::TestFixture is written that provides one or more tests for a specific class. However, there are several tricks to writing tests that will make them much easier to use. These help improve the interactive use of the tests, simplifying finding problems when the tests are run as part of an automated build, since these tests often become a kind of regression test once a class has been developed.

Some parts of these instructions are specific to OPeNDAP and its source code for libdap and the BES, but those parts are limited to code examples and it is easy to see how the same ideas could be applied to similar code that differs in its specifics.

1.1 CppUnit basic layout

The example below shows a complete unit test written with CppUnit. Using the helper macros streamlines writing the tests (but also introduces some wrinkles we discuss later on).

When writing tests, make the default behavior as quiet as possible since the tests will likely be run by an automated build. Use a debug macro or switch to control output of instrumentation.

Here are the main sections of a CppUnit test:

1.1.1 Header files

To get started, include header files so that you can subclass TestFixture and use the CppUnit macros.

#include "config.h"        // Build by autoconf

#include <cppunit/TextTestRunner.h>
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/extensions/HelperMacros.h>

Then, include the header that defines the class you want to test, other debug tools you use and whatever is needed for getopt in you build/target environment. Also define static booleans for things like debug switches and other run-time options. Adding a debug option is good because these tests are often used to track down problems as well as being used as part of automated builds. A debug option lets people see what's wrong without gumming up the works when a machine runs the tests.

#include "AttrTable.h"     // The class to test
#include "debug.h"         // Debug macros

#include "GetOpt.h"        // getopt C++ wrapper

static bool debug = false; // Set down in main()

#undef DBG
#define DBG(x) do { if (debug) (x); } while(false);

1.1.2 Subclassing TestFixture =

1.1.3 Using macros

1.1.4 Main

1.1.5 Building the code

1.2 Running tests by name

To run a test by name, where 'test' means one of the methods in the TestFixture class, pass the method name to TestRunner::run(). For example, if you have a TestRunner named runner, a TestFixture class named MyTests and it has methods void test_1() {} and void test_2() {}, you would run those by calling runner.run("myTest::test_1") and runner.run(:myTes::test_2"). This will work if the test source file uses the macro CPPUNIT_TEST_SUITE( MyTest ); to declare the TestFixure and CPPUNIT_TEST(test_1);, ..., to declare the individual tests. Note that after the TestFixure class is defined, the macro CPPUNIT_TEST_SUITE_REGISTRATION(MyTest); must be used.

1.3 Using command line switches to control output

Look in the example to see how I use Gnu's GetOpt to process command line options. In the code, '-d' is used to turn on debugging. When on, a static global is set to true and a macro writes various instrumentation to stderr. Here's the global (file scope) and macro:

static bool debug = false;

#undef DBG
#define DBG(x) do { if (debug) (x); } while(false);

The #undef DBG is there because the #define will redefine the DBG macro, changing it from the definition provided by debug.h.

and the code to process the switch and call tests if they are named on the command line:

int main(int argc, char*argv[]) {
    CppUnit::TextTestRunner runner;
    runner.addTest(CppUnit::TestFactoryRegistry::getRegistry().makeTest());

    GetOpt getopt(argc, argv, "d");
    char option_char;
    while ((option_char = getopt()) != EOF)
        switch (option_char) {
        case 'd':
            debug = 1;  // debug is a static global
            break;
        default:
            break;
        }

    bool wasSuccessful = true;
    string test = "";
    int i = getopt.optind;
    if (i == argc) {
        // run them all
        wasSuccessful = runner.run("");
    }
    else {
        while (i < argc) {
            test = string("D4DimensionsTest::") + argv[i++];

            wasSuccessful = wasSuccessful && runner.run(test);
        }
    }

    return wasSuccessful ? 0 : 1;
}

The first loop processes the option; the if will call runner.run with "" which runs all the tests if there are no more arguments or will assume that remaining arguments are test names and will iterate over them, calling each in turn.


Note: The "-d" switch and calling the tests by name will not work with make. You have to run the test code from the command line by hand. If your unit test file is named testy.cc the you'll need to change directory to the unit-tests dir and do a ./testy -d testName to see the effects of these changes.

1.4 Putting it all together

#include "config.h"

#include <cppunit/TextTestRunner.h>
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/extensions/HelperMacros.h>

#include <GetOpt.h> // Part of libdap

//#define DODS_DEBUG

#include "D4Dimensions.h"
#include "XMLWriter.h"

#include "Error.h"
#include "debug.h"

#include "testFile.h"
#include "test_config.h"

using namespace CppUnit;
using namespace std;
using namespace libdap;

static bool debug = false;

#undef DBG
#define DBG(x) do { if (debug) (x); } while(false);

class D4DimensionsTest: public TestFixture {
private:
    XMLWriter *xml;
    D4Dimensions *d;

public:
    D4DimensionsTest() {
    }

    ~D4DimensionsTest() {
    }

    void setUp() {
        d = new D4Dimensions;
        xml = new XMLWriter;
    }

    void tearDown() {
        delete xml;
        delete d;
    }

    void test_print_copy_ctor() {
        d->add_dim_nocopy(new D4Dimension("first", 10));
        d->add_dim_nocopy(new D4Dimension("second", 100));
        d->add_dim_nocopy(new D4Dimension("third"));

        D4Dimensions lhs(*d);

        lhs.print_dap4(*xml);
        string doc = xml->get_doc();
        string baseline = readTestBaseline(string(TEST_SRC_DIR) + "/D4-xml/D4Dimensions_3.xml");
        DBG(cerr << "test_print_copy_ctor: doc: " << doc << endl);
        DBG(cerr << "test_print_copy_ctor: baseline: " << baseline << endl);
        CPPUNIT_ASSERT(doc == baseline);
    }

    CPPUNIT_TEST_SUITE( D4DimensionsTest );

        CPPUNIT_TEST(test_print_empty);

        // CPPUNIT_TEST_EXCEPTION( test_error, Error );
 
    CPPUNIT_TEST_SUITE_END();
};

CPPUNIT_TEST_SUITE_REGISTRATION(D4DimensionsTest);

int main(int argc, char*argv[]) {
    CppUnit::TextTestRunner runner;
    runner.addTest(CppUnit::TestFactoryRegistry::getRegistry().makeTest());

    GetOpt getopt(argc, argv, "d");
    char option_char;

    while ((option_char = getopt()) != EOF)
        switch (option_char) {
        case 'd':
            debug = 1;  // debug is a static global
            break;
        default:
            break;
        }

    bool wasSuccessful = true;
    string test = "";
    int i = getopt.optind;
    if (i == argc) {
        // run them all
        wasSuccessful = runner.run("");
    }
    else {
        while (i < argc) {
            test = string("D4DimensionsTest::") + argv[i++];

            wasSuccessful = wasSuccessful && runner.run(test);
        }
    }

    return wasSuccessful ? 0 : 1;
}