A system is usually designed as a collection of subsystems (e.g., packages, namespaces, libraries, files). A subsystem is a collection of classes, functions, and sub-subsystems, etc:
A testing framework for such a system is also organized into a hierarch. The top level system test runs tests of all of the subsystems. A subsystem test runs tests of each of its classes, functions, and sub-subsystems. A class test runs tests of each of its member functions:
Subsystem tests-- tests that run tests of member components-- are called test suites. Tests that test a single class or function are called unit tests.
Note: In this paper we are talking about functional or black-box testing, not structural testing.
Assume a library consists of three sub-libraries: Math1, Math2, and Strings. Math1 contains three global functions: fun1, fun2, and fun3. The Math2 library only contains two functions: fun4 and fun5. Finally, the string library contains fun6 and fun7. The author of each function also writes a constructor that generates a unit test for that function: makeFun1Test(), makeFun2Test(), etc. A constructor associated with each sub-library creates a suite test containing all of the unit tests for functions in that library. Finally, a system test containing all of the sub-library tests is created and run.
#include "test.h"
double fun1(double x);
double fun2(double x);
double fun3(double x);
Test* makeMathTest();
#include "test.h"
double fun4(double x);
double fun5(double x);
Test* makeMath2Test();
#include "test.h"
#include <string>
using namespace std;
int fun6(string s);
string fun7(string s);
Test* makeStringTest();
#include "math1.h"
double fun1(double x) { return 2 * x * x + 3 * x + 4; }
double fun2(double x) { return -2 * x + 9; }
double fun3(double x) { return x * x * x - x * x + x; }
// And now the tests:
Test* makeFun1Test()
{
UnitTest<double, double>* test
= new UnitTest<double,
double>(fun1, "fun1 test");
(*test)[0] = 4;
(*test)[1] = 9;
(*test)[-1] = 3;
return test;
}
Test* makeFun2Test()
{
UnitTest<double, double>* test
= new UnitTest<double,
double>(fun2, "fun2 test");
(*test)[2] = 5;
(*test)[0] = 9;
(*test)[5] = 0; // oops!
(*test)[50] = -91;
return test;
}
Test* makeFun3Test()
{
UnitTest<double, double>* test
= new UnitTest<double,
double>(fun3, "fun3 test");
(*test)[1] = 1;
(*test)[0] = 0;
(*test)[2] = 6;
(*test)[50] = -91; // oops!
return test;
}
Test* makeMath1Test()
{
SuiteTest* test = new
SuiteTest("math1 tests");
test->add(makeFun1Test());
test->add(makeFun2Test());
test->add(makeFun3Test());
return test;
}
#include "math2.h"
double fun4(double x) { return (x + 1) * (x - 1); }
double fun5(double x) { return .5 * x + 1; }
// And now the tests:
Test* makeFun4Test()
{
UnitTest<double, double>* test
= new UnitTest<double,
double>(fun4, "fun4 test");
(*test)[3] = 8;
(*test)[1] = 0;
(*test)[.3] = -.91;
(*test)[5] = 7; // oops
// etc.
return test;
}
Test* makeFun5Test()
{
UnitTest<double, double>* test
= new UnitTest<double,
double>(fun5, "fun5 test");
(*test)[1] = 1.5;
(*test)[2] = 2;
(*test)[.1] = 1.05;
// etc.
return test;
}
Test* makeMath2Test()
{
SuiteTest* test = new
SuiteTest("math2 tests");
test->add(makeFun4Test());
test->add(makeFun5Test());
return test;
}
#include "strings.h"
int fun6(string s) { return s.length() + 1; }
string fun7(string s) { return s + s; }
// And now the tests:
Test* makeFun6Test()
{
UnitTest<string, int>* test
= new UnitTest<string,
int>(fun6, "unit test 6");
(*test)["hello"] = 6;
(*test)["Mississippi"] = 12;
return test;
}
Test* makeFun7Test()
{
UnitTest<string, string>* test
= new UnitTest<string,
string>(fun7, "unit test 7");
(*test)["hello"] =
"hellohello";
(*test)["Mississippi"] =
"MississippiMississippi";
return test;
}
Test* makeStringTest()
{
SuiteTest *suite = new
SuiteTest("string tests");
suite->add(makeFun6Test());
suite->add(makeFun7Test());
return suite;
}
#include "math1.h"
#include "math2.h"
#include "strings.h"
#include "test.h"
int main()
{
SuiteTest systemTest("System
Test");
systemTest.add(makeMath1Test());
systemTest.add(makeMath2Test());
systemTest.add(makeStringTest());
systemTest.run();
cout << systemTest;
return 0;
}
vvvvvvvvvvvvvvvvvvvvvvvvvv
Test suite name: System Test
#passes = 1
#fails = 2
Details:
vvvvvvvvvvvvvvvvvvvvvvvvvv
Test suite name: math fun tests
#passes = 1
#fails = 2
Details:
... Unit test fun1 test passed
... Unit test fun2 test
..... on input 5 expected 0 but computed -1
... Unit test fun3 test
..... on input 50 expected -91 but computed 122550
^^^^^^^^^^^^^^^^^^^^^^^^^^
vvvvvvvvvvvvvvvvvvvvvvvvvv
Test suite name: math2 tests
#passes = 1
#fails = 1
Details:
... Unit test fun4 test
..... on input 0.3 expected -0.91 but computed -0.91
..... on input 5 expected 7 but computed 24
... Unit test fun5 test passed
^^^^^^^^^^^^^^^^^^^^^^^^^^
vvvvvvvvvvvvvvvvvvvvvvvvvv
Test suite name: string tests
#passes = 2
#fails = 0
Details:
... Unit test unit test 6 passed
... Unit test unit test 7 passed
^^^^^^^^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^^^^^^^
CPPUnit is based on the Composite Design Pattern:
Note that a suite test may contain other suite tests as well as unit tests.
A unit test compares the actual outputs produced by a single function with the expected outputs. The inputs, together with the expected outputs, are stored in a table called the oracle. The types of the inputs and outputs are given by template parameters in and out.
Finally, the function to be tested is a member variable rather than a member function. This is called the Pluggable Adapter Design pattern.
#ifndef TEST_H
#define TEST_H
#include <iostream>
#include <string>
#include <list>
#include <map>
using namespace std;
class Test { ... };
class SuiteTest extends Test{ ... };
ostream& operator<<(ostream& os, const Test& test);
template <typename InputType, typename OutputType>
class UnitTest: public Test { ... };
template <typename InputType, typename OutputType>
bool UnitTest<InputType, OutputType>::run()
{ ... }
template <typename InputType, typename OutputType>
void UnitTest<InputType, OutputType>::display(ostream&
os) const
{ ... }
#endif
class Test
{
public:
Test(string name = "no name")
{
this->name = name;
}
virtual bool run() = 0;
string getName() const { return name; }
virtual void display(ostream& os =
cout) const {}
protected:
string name;
};
class SuiteTest: public Test
{
public:
SuiteTest(string name = "suite
test")
:Test(name)
{
passes = fails = 0;
}
bool run();
void add(Test* test) {
tests.push_back(test); }
virtual void display(ostream& os =
cout) const {}
private:
list<Test*> tests;
int passes, fails;
};
bool SuiteTest::run()
{
list<Test*>::iterator p;
for(p = tests.begin(); p !=
tests.end(); p++)
{
bool temp = (*p)->run();
if (!temp)
{
fails++;
}
else
{
passes++;
}
result = result && temp;
}
return result;
}
typedef double (*R2RFun)(double);
class R2RUnitTest: public Test
{
public:
R2RUnitTest(R2RFun f, string name =
"unit test")
: Test(name)
{
fun = f;
delta = 1e-10;
}
bool run();
double& operator[](double d)
{
return oracle[d];
}
void setDelta(double d) { delta = d; }
double getDelta() const { return delta;
}
protected:
map<double, double> oracle;
R2RFun fun;
double delta;
};
bool R2RUnitTest::run()
{
bool result = true;
map<double, double>::iterator p;
for(p = oracle.begin(); p !=
oracle.end(); p++)
{
double input = (*p).first;
double expected = (*p).second;
double output = fun(input);
bool
temp = fabs(expected - output) <= delta;
if (!temp)
{
cout << "...error:
" << name << " expected " << expected;
cout << " but computed
" << output << endl;
}
result &&= temp;
}
return result;
}
template <typename InputType, typename OutputType>
class UnitTest: public Test
{
public:
typedef OutputType (*Fun)(InputType);
UnitTest(Fun f, string name =
"unit test")
: Test(name)
{
fun = f;
}
class FailureReport
{
public:
FailureReport(InputType i,
OutputType e, OutputType c)
{
expected = e;
computed = c;
input = i;
}
OutputType expected;
OutputType computed;
InputType input;
};
bool run();
OutputType& operator[](InputType d)
{
return oracle[d];
}
void display(ostream& os = cout)
const;
protected:
map<InputType, OutputType>
oracle;
list<ErrorReport*> failures;
Fun fun;
};
template <typename InputType, typename OutputType>
bool UnitTest<InputType, OutputType>::run()
{
bool result = true;
map<InputType,
OutputType>::iterator p;
for(p = oracle.begin(); p !=
oracle.end(); p++)
{
InputType input = (*p).first;
OutputType expected = (*p).second;
OutputType output = fun(input);
bool temp = expected == output;
if (!temp)
{
failures.push_back(
new FailureReport(input,
expected, output));
}
result = result && temp;
}
return result;
}
template <typename InputType, typename OutputType>
void UnitTest<InputType, OutputType>::display(ostream& os) const
{
os << "... Unit test "
<< name;
if (failures.empty())
{
os << " passed\n";
}
else
{
list<FailureReport*>::const_iterator p;
os << endl;
for(p = failures.begin(); p != failures.end(); p++)
{
os << "..... on input
" << (*p)->input;
os << " expected " << (*p)->expected;
os << " but computed " << (*p)->computed
<< endl;
}
}
}
void SuiteTest::display(ostream& os) const
{
os <<
"vvvvvvvvvvvvvvvvvvvvvvvvvv\n";
os << "Test suite name:
" << name << endl;
os << " #passes = " << passes <<
endl;
os << " #fails = " << fails <<
endl;
os << "Details:\n";
list<Test*>::const_iterator p;
for(p = tests.begin(); p !=
tests.end(); p++)
{
(*p)->display(os);
}
os << "^^^^^^^^^^^^^^^^^^^^^^^^^^\n";
}
ostream& operator<<(ostream& os, const Test&
test)
{
test.display(os);
return os;
}