Unit Testing

This module provides a complete framework for effective Unit testing. Data-driven, as well as and protocol-driven testing is possible. Unit tests can easily be integrated into the build process and test results can be reported and logged. The output format for reports and logs is configurable.

A Simple Test Case

The TestCase class can be used to implement simple unit test. This is simply done by deriving from Pt::Unit::TestCase and implementing the test() method. The member functions setUp() and tearDown() can be used to manage any resources the test might require. The PT_UNIT_ASSERT macro can be used to assert test conditions during the test.

class MyTestCase : public Pt::Unit::TestCase
{
public:
MyTestCase()
: Pt::Unit::TestCase("MyTestCase")
, _a(0), _b(0)
{ }
void setUp()
{
_a = 5;
_b = 5;
}
void tearDown()
{
}
void test()
{
_a += _b;
PT_UNIT_ASSERT(_a == 10);
}
private:
int _a;
int _b;
};

Build System Integration

The Jam build system containes rules to build a unit test and have it run automatically after it has been build. For programs are build as usual with the rule Main. The rule RunUnitTest can be used in a Jamfile to let the unit test execute during the build:

Main mytest : mytest.cpp ;
RunUnitTest Pt-test : $(TEST_OUTPUT_DIR) ;

This will cause the jam tool to report a failed build, if the unit test does not succeed. The second argument for the RunUnitTest rule is the directory, where the test output will be written to. This way unit tests can easily be integrated in automated builds.

Data Driven Testing

Sometimes it is desirable to repeat a test with different data. This is achieved by registering methods that take arguments (the test data) in a Pt::Unit::TestSuite.

class MyTestSuite : public Pt::Unit::TestSuite
{
public:
MyTestSuite()
: Pt::Unit::TestSuite("MyTestSuite")
{
this->registerMethod("Addition", *this, &MyTestSuite::Addition);
}
void Addition(int a, int b)
{
// testing code
}
};

A protocol can then by used to call the Addition() method multiple times with different data. The data has to be provided as Pt::SerializationInfo objects, which works for many types without additional code.

class MyTestProtocol : public Pt::Unit::TestProtocol
{
public:
void run(Pt::Unit::TestSuite& suite)
{
SerializationInfo data[2];
data[0] <<= 4;
data[1] <<= 9;
suite.runTest( "MyTestSuite::Addition", data, 2 );
data[0] <<= 13;
data[1] <<= 2;
suite.runTest( "MyTestSuite::Addition", data, 2 );
}
};

When this protocol is applied to MyTestSuite, the Addition() method will be called twice, each time with different test data. It is often helpful to read the test data from a file or another data source, instead of hardcoding it. The next paragraph explains how a protocol is applied to a test.

Protocol Driven Testing

Protocol driven testing implements the idea to control the order in which tests are executed, or to run test methods multiple times. To accomplish this a protocol is defined by deriving from the Pt::Unit::TestProtocol class.The following example will run a test called 'MyTest' three times and sleeps between the tests for 1 second.

#include <Pt/Unit/TestProtocol.h>
#include <Pt/System/Process.h>
class MyProtocol : public Pt::Unit::TestProtocol
{
public:
MyProtocol()
{}
void run(Pt::Unit::TestSuite& suite)
{
suite.runTest( "MyTestMethod" );
suite.runTest( "MyTestMethod" );
suite.runTest( "MyTestMethod" );
}
};

The methods are resolved by the name they have been registered with. If a test can not be executed through the TestSuite::runTest() method an exception of the type std::runtime_error is thrown and the test fails if the exception is allowed to propagate out of the TestProtocol::run() method. The custom protocol can be assigned to a TestSuite in the constructor:

class MyTestSuite
{
public:
MyTestSuite()
: Pt::Unit::TestSuite("MyTestSuite", protocol)
{
this->registerMethod("MyTestMethod", *this, &MyTestSuite::MyTestMethod);
this->setProtocol(_protocol);
}
void MyTestMethod()
{
// testing code
}
private:
MyProtocol _protocol;
};

A test protocol can be set using the TestSuite::setProtocol() method. It is therefore possible to load protocols from a custom source and assign them before the test suite is run.

Running Tests

All tests can be run by the Application object of the Pt::Unit module. To register a test with the application object the RegisterTest class template can be used or Application::registerTest can be called. A typical test program will instanciate an Application object and set reporters for result reporting and logging.

int main()
{
std::ofstream fs("log.txt");
Pt::Unit::Reporter reporter(fs);
app.setReporter(reporter);
return app.run();
}

For convenience, the header file TestMain.h already contains such a main() function, where reporters can be selected by command line arguments. So the implementer of a test only has to include TestMain.h in the source code, where the main() function would normally be.