The Classical Approach
The way to use Scrunitizer which is most similar to what is done
in JUnit (and other framework heavily inspired by JUnit) is to
create tests by writing unit test classes.
In almost all unit test frameworks, tests are defined by specifying test case classes
that implement the according test functionality.
So first a test case that tests the equals function the classical way:
class ClassicEqualsTest : public Scrunitizer::TestCase {
private:
Rect* r1;
Rect* r2;
Rect* r3;
public:
ClassicEqualsTest () : TestCase ("Equals Test") { }
void setUp () {
r1 = new Rect (0, 0, 10, 10);
r2 = new Rect (0, 0, 10, 10);
r3 = new Rect (0, 0, 10, 20);
}
void test () {
assert (r1->equals (*r2), "r1 == r2");
assert (r1->equals (*r1), "r1 == r1");
assert (r2->equals (*r1), "r1 == r2");
assert (! r1->equals (*r3), "r1 != r3");
assert (! r3->equals (*r1), "r1 != r3");
}
void tearDown () {
delete r1;
delete r2;
delete r3;
}
};
That's quite straightforward and perfectly valid.
- First, some rectangle instances are created in the setUp function,
- in the test function this rectangles are used for assertions and
- finally they are destroyed in the tear down function.
But thatīs quite a lot of code, and - more important - rectangle pointers
are used, so that using the rectangle functions (which use references as parameters)
becomes a bit odd.
So why not use member variables that are initialised in the constructor. A test case that tests the
covers function would look like this:
class DangerousCoversTest : public Scrunitizer::TestCase {
private:
Rect cover;
Rect notCovered;
Rect partlyCovered;
Rect fullyCovered;
public:
DangerousCoversTest () :
TestCase ("Covers Test"),
cover ( 0, 0, 100, 100),
notCovered (150, 150, 50, 50),
partlyCovered ( 50, 50, 100, 100),
fullyCovered ( 50, 50, 50, 50) {
}
void test () {
assert (! cover.covers (notCovered),
"! ( 0, 0, 100, 100) covers (150, 150, 200, 200)");
assert (! cover.covers (partlyCovered),
"! ( 0, 0, 100, 100) covers ( 50, 50, 150, 150)");
assert ( cover.covers (fullyCovered),
"( 0, 0, 100, 100) covers ( 50, 50, 100, 100)");
}
};
Well - that's valid C++ code in typical coding C++ style. But it is not a good test case!
The reason why such a kind of test case is problematic is that a part of the domain functionality,
the rectangle constructor, is executed when the test case is instantiated! But at that point in time
the unit test framework is not running. In this simple example it does not matter, but in case of a
complex constructor which does a lot of things, which could even throw exceptions or crash, this
is a real problem. So stay away from this kind of test cases.
A better example below:
class ClassicCoversTest : public Scrunitizer::TestCase {
public:
ClassicCoversTest () : TestCase ("Covers Test") { }
void test () {
Rect cover ( 0, 0, 100, 100);
Rect notCovered (150, 150, 50, 50);
Rect partlyCovered ( 50, 50, 100, 100);
Rect fullyCovered ( 50, 50, 50, 50);
assert (! cover.covers (notCovered),
"! ( 0, 0, 100, 100) covers (150, 150, 200, 200)");
assert (! cover.covers (partlyCovered),
"! ( 0, 0, 100, 100) covers ( 50, 50, 150, 150)");
assert ( cover.covers (fullyCovered),
"( 0, 0, 100, 100) covers ( 50, 50, 100, 100)");
}
};
Here the test code looks better. And constructing the rectangle instances in the test function
is also OK because creating a rectangle is neither complex nor costly.
Assuming that both tests should be added to a test suite, the following code is needed in addition:
Scrunitizer::TestSuite* classic = new Scrunitizer::TestSuite ("Classic Tests");
classic->add (new ClassicEqualsTest ());
classic->add (new ClassicCoversTest ());
If you look at the ClassicCoversTest class example, you might notice that apart from the test function
all the rest is more or less additional burden that is only needed because the test functionality must
be bound to test case classes. But thereīs another way to create test cases...
The Function Bases Approach
One way to build unit tests is to put the test functionality in a plain function.
That's not very object oriented, but for unit tests that do not need a
complicated fixture set-up (and fixture tear-down), thatīs quite OK.
It looks like that:
void EqualsTestFunction () {
Rect r1 (0, 0, 10, 10);
Rect r2 (0, 0, 10, 10);
Rect r3 (0, 0, 10, 20);
assert (r1.equals (r2), "r1 == r2");
assert (r1.equals (r1), "r1 == r1");
assert (r2.equals (r1), "r1 == r2");
assert (! r1.equals (r3), "r1 != r3");
assert (! r3.equals (r1), "r1 != r3");
}
void CoversTestFunction () {
Rect cover ( 0, 0, 100, 100);
Rect notCovered (150, 150, 50, 50);
Rect partlyCovered ( 50, 50, 100, 100);
Rect fullyCovered ( 50, 50, 50, 50);
assert (! cover.covers (notCovered),
"! ( 0, 0, 100, 100) covers (150, 150, 200, 200)");
assert (! cover.covers (partlyCovered),
"! ( 0, 0, 100, 100) covers ( 50, 50, 150, 150)");
assert ( cover.covers (fullyCovered),
"( 0, 0, 100, 100) covers ( 50, 50, 100, 100)");
}
And how do I create tests from that functions? Well, by instantiating a TestCaseAdapter template
with the functions for test, setUp and tearDown as parameters. Sounds complicated?. To ease the pain
of manually calling template class constructors, some helper functions are defined. The
makeTestCase function (a template function) takes the name of the test case and a function
as parameter, constructs an according test case class and finally creates a test case instance.
There are also versions that take an additional setUp and an setUp and tearDown function as
parameters.
For the two test functions above, it goes like that:
Scrunitizer::TestSuite* functionBased = new Scrunitizer::TestSuite ("Function based Tests");
functionBased->add (makeTestCase ("Function based equals Test", EqualsTestFunction));
functionBased->add (makeTestCase ("Function based covers Test", CoversTestFunction));
That's quite good because itīs short. Only one function and one line of code to create and
register (add) a test case.
void testIntersectionPred () {
Rect centerRect (0, 0, 10, 10);
Rect testRectNW (-10, -10, 10, 10);
Rect testRectN ( 0, -10, 10, 10);
Rect testRectNE ( 10, -10, 10, 10);
Rect testRectW (-10, 0, 10, 10);
Rect testRectC ( 0, 0, 10, 10);
Rect testRectE ( 10, 0, 10, 10);
Rect testRectSW (-10, 10, 10, 10);
Rect testRectS ( 0, 10, 10, 10);
Rect testRectSE ( 10, 10, 10, 10);
assert (! centerRect.intersects (testRectNW),
"Failure in NW intersection");
assert (! centerRect.intersects (testRectN ),
"Failure in N intersection");
assert (! centerRect.intersects (testRectNE),
"Failure in NE intersection");
assert (! centerRect.intersects (testRectW),
"Failure in W intersection");
assert ( centerRect.intersects (testRectC ),
"Failure in C intersection");
assert (! centerRect.intersects (testRectE),
"Failure in E intersection");
assert (! centerRect.intersects (testRectSW),
"Failure in SW intersection");
assert (! centerRect.intersects (testRectS ),
"Failure in S intersection");
assert (! centerRect.intersects (testRectSE),
"Failure in SE intersection");
}
Regsitering this function is quite simple:
The Exhaustive Approach
That could be addressed by splitting up the test function in nine smaller ones:
void testNWIntersection () {
Rect centerRect (0, 0, 10, 10);
Rect testRectNW (-10, -10, 10, 10);
assert (! centerRect.intersects (testRectNW),
"Failure in NW intersection");
}
void testNIntersection () {
Rect centerRect (0, 0, 10, 10);
Rect testRectN ( 0, -10, 10, 10);
assert (! centerRect.intersects (testRectN ),
"Failure in N intersection");
}
void testNEIntersection () {
Rect centerRect (0, 0, 10, 10);
Rect testRectNE ( 10, -10, 10, 10);
assert (! centerRect.intersects (testRectNE),
"Failure in NE intersection");
}
void testWIntersection () {
Rect centerRect (0, 0, 10, 10);
Rect testRectW (-10, 0, 10, 10);
assert (! centerRect.intersects (testRectW),
"Failure in W intersection");
}
void testCIntersection () {
Rect centerRect (0, 0, 10, 10);
Rect testRectC ( 0, 0, 10, 10);
assert ( centerRect.intersects (testRectC ),
"Failure in C intersection");
}
void testEIntersection () {
Rect centerRect (0, 0, 10, 10);
Rect testRectE ( 10, 0, 10, 10);
assert (! centerRect.intersects (testRectE),
"Failure in E intersection");
}
void testSWIntersection () {
Rect centerRect (0, 0, 10, 10);
Rect testRectSW (-10, 10, 10, 10);
assert (! centerRect.intersects (testRectSW),
"Failure in SW intersection");
}
void testSIntersection () {
Rect centerRect (0, 0, 10, 10);
Rect testRectS ( 0, 10, 10, 10);
assert (! centerRect.intersects (testRectS ),
"Failure in S intersection");
}
void testSEIntersection () {
Rect centerRect (0, 0, 10, 10);
Rect testRectSE ( 10, 10, 10, 10);
assert (! centerRect.intersects (testRectSE),
"Failure in SE intersection");
}
They could be combined to a test suite as shown below:
Scrunitizer::TestSuite* exhFuncBased = new Scrunitizer::TestSuite ("Exhaustive function based");
exhFuncBased->add (makeTestCase ("NW intersection test", testNWIntersection));
exhFuncBased->add (makeTestCase ("N intersection test", testNIntersection));
exhFuncBased->add (makeTestCase ("NE intersection test", testNEIntersection));
exhFuncBased->add (makeTestCase ("W intersection test", testWIntersection));
exhFuncBased->add (makeTestCase ("C intersection test", testCIntersection));
exhFuncBased->add (makeTestCase ("E intersection test", testEIntersection));
exhFuncBased->add (makeTestCase ("SW intersection test", testSWIntersection));
exhFuncBased->add (makeTestCase ("S intersection test", testSIntersection));
exhFuncBased->add (makeTestCase ("SE intersection test", testSEIntersection));
The big disadvantage is still that 9 test functions have to be written only for the
intersection tests.
The according examples for the coverage test are left out here.
The Parametrised Test Function Approach
But youīve might guessed it already: Thereīs also a solution for this problem: Parametrised
test functions. It is possible to bind 1 to arguments of a function when it is called
during execution of a test case. The convenience function binddoes the job. It
automatically creates a function call adapter (a class template) that is used instead
of the original function. To make use of that feature, a parametrised version of the test
function is needed:
void testIntersects (int xc, int yc, bool expectedResult) {
Rect centerRect (0, 0, 10, 10);
Rect testRect (xc * 10, yc * 10, 10, 10);
assert (centerRect.intersects (testRect) == expectedResult,
"Failure in intersection test");
}
void testCovers (int xc, int yc, bool expectedResult) {
Rect centerRect (0, 0, 10, 10);
Rect testRect (xc * 10, yc * 10, 10, 10);
assert (centerRect.covers (testRect) == expectedResult,
"Failure in coverage test");
}
Now, creating the test cases looks like that:
Scrunitizer::TestSuite* paramFuncBased = new Scrunitizer::TestSuite("Parameterised function based");
paramFuncBased->add (makeTestCase ("NW intersection test", bind (testIntersects, -1, -1, false)));
paramFuncBased->add (makeTestCase ("N intersection test", bind (testIntersects, 0, -1, false)));
paramFuncBased->add (makeTestCase ("NE intersection test", bind (testIntersects, 1, -1, false)));
paramFuncBased->add (makeTestCase ("W intersection test", bind (testIntersects, -1, 0, false)));
paramFuncBased->add (makeTestCase ("C intersection test", bind (testIntersects, 0, 0, true )));
paramFuncBased->add (makeTestCase ("E intersection test", bind (testIntersects, 1, 0, false)));
paramFuncBased->add (makeTestCase ("SW intersection test", bind (testIntersects, -1, 1, false)));
paramFuncBased->add (makeTestCase ("S intersection test", bind (testIntersects, 0, 1, false)));
paramFuncBased->add (makeTestCase ("SE intersection test", bind (testIntersects, 1, 1, false)));
paramFuncBased->add (makeTestCase ("NW coverage test", bind (testCovers, -1, -1, false)));
paramFuncBased->add (makeTestCase ("N coverage test", bind (testCovers, 0, -1, false)));
paramFuncBased->add (makeTestCase ("NE coverage test", bind (testCovers, 1, -1, false)));
paramFuncBased->add (makeTestCase ("W coverage test", bind (testCovers, -1, 0, false)));
paramFuncBased->add (makeTestCase ("C coverage test", bind (testCovers, 0, 0, true )));
paramFuncBased->add (makeTestCase ("E coverage test", bind (testCovers, 1, 0, false)));
paramFuncBased->add (makeTestCase ("SW coverage test", bind (testCovers, -1, 1, false)));
paramFuncBased->add (makeTestCase ("S coverage test", bind (testCovers, 0, 1, false)));
paramFuncBased->add (makeTestCase ("SE coverage test", bind (testCovers, 1, 1, false)));
The Object Based Approach
What is possible with plain functions could also be done with member functions and fixture objects.
Sometimes it might be nice to share helper functions between several test functions or to
save state information between, set-up, test execution and tear-down. That could be done
by combining all this elements to a test fixture class. The previous example as a test fixture
and handler class is shown below:
class RectangleTester {
public:
void testIntersects (int cx, int cy, bool expectedResult) {
Rect centerRect (0, 0, 10, 10);
Rect testRect (cx * 10, cy * 10, 10, 10);
assert (centerRect.intersects (testRect) == expectedResult,
"Failure in intersection test");
}
void testCovers (int cx, int cy, bool expectedResult) {
Rect centerRect (0, 0, 10, 10);
Rect testRect (cx * 10, cy * 10, 10, 10);
assert (centerRect.covers (testRect) == expectedResult,
"Failure in coverage test");
}
};
Again, creating the test cases is done the following way:
Scrunitizer::TestSuite* objBased = new Scrunitizer::TestSuite ("Parametrized object based Tests");
objBased->add (makeTestGadget ("NW", apply (&RectangleTester::testIntersects, -1, -1, false)));
objBased->add (makeTestGadget ("N", apply (&RectangleTester::testIntersects, 0, -1, false)));
objBased->add (makeTestGadget ("NE", apply (&RectangleTester::testIntersects, 1, -1, false)));
objBased->add (makeTestGadget ("W", apply (&RectangleTester::testIntersects, -1, 0, false)));
objBased->add (makeTestGadget ("C", apply (&RectangleTester::testIntersects, 0, 0, true )));
objBased->add (makeTestGadget ("E", apply (&RectangleTester::testIntersects, 1, 0, false)));
objBased->add (makeTestGadget ("SW", apply (&RectangleTester::testIntersects, -1, 1, false)));
objBased->add (makeTestGadget ("S", apply (&RectangleTester::testIntersects, 0, 1, false)));
objBased->add (makeTestGadget ("SE", apply (&RectangleTester::testIntersects, 1, 1, false)));
objBased->add (makeTestGadget ("NW", apply (&RectangleTester::testCovers, -1, -1, false)));
objBased->add (makeTestGadget ("N", apply (&RectangleTester::testCovers, 0, -1, false)));
objBased->add (makeTestGadget ("NE", apply (&RectangleTester::testCovers, 1, -1, false)));
objBased->add (makeTestGadget ("W", apply (&RectangleTester::testCovers, -1, 0, false)));
objBased->add (makeTestGadget ("C", apply (&RectangleTester::testCovers, 0, 0, true )));
objBased->add (makeTestGadget ("E", apply (&RectangleTester::testCovers, 1, 0, false)));
objBased->add (makeTestGadget ("SW", apply (&RectangleTester::testCovers, -1, 1, false)));
objBased->add (makeTestGadget ("S", apply (&RectangleTester::testCovers, 0, 1, false)));
objBased->add (makeTestGadget ("SE", apply (&RectangleTester::testCovers, 1, 1, false)));
top->add (objBased);
Test case classes that are generated by applying member functions to test fixture class instances
are called test gadgets.
Instead of bind, the convenience function apply is used
to assign parameter values. One difference is that apply must also be used if no parameters
are needed for a member function. The is necessary because Scrunitizer uses a special interface,
Applicable, to apply member functions to objects.
The Fixture Class Based Approach
Well, this does not look like a big step forward. To make real use out of fixture object, their
main advantage must be used; the ability to hold state and configuration information. A bit more
advanced version of the previous test class is shown below:
class BasicRectTester {
protected:
virtual void setUpCenterRect (Rect& r) {
r.reshape (0, 0, 10, 10);
}
virtual void setUpTestRect (Rect& r, int cx, int cy) {
r.reshape (cx * 10, cy * 10, 10, 10);
}
virtual bool expectedIntersectsResult (int cx, int cy) {
return cx == 0 && cy == 0;
}
virtual bool expectedCoversResult (int cx, int cy) {
return cx == 0 && cy == 0;
}
public:
void testIntersects (int cx, int cy) {
Rect centerRect;
Rect testRect;
setUpCenterRect (centerRect);
setUpTestRect (testRect, cx, cy);
assert (centerRect.intersects (testRect) == expectedIntersectsResult (cx, cy),
"Failure in intersection test");
}
void testCovers (int cx, int cy) {
Rect centerRect (0, 0, 10, 10);
Rect testRect;
setUpCenterRect (centerRect);
setUpTestRect (testRect, cx, cy);
assert (centerRect.covers (testRect) == expectedCoversResult (cx, cy),
"Failure in coverage test");
}
public:
static Scrunitizer::TestSuite* makePredicateTests (
const string& suiteName,
const string& testNamePrefix,
void (BasicRectTester::*testFunc)(int,int),
BasicRectTester* fixture
);
};
Scrunitizer::TestSuite* BasicRectTester::makePredicateTests (
const string& suiteName,
const string& testNamePrefix,
void (BasicRectTester::*testFunc)(int,int),
BasicRectTester* fixture
) {
using Scrunitizer::apply;
using Scrunitizer::makeTestGadget;
Scrunitizer::TestSuite* suite = new Scrunitizer::TestSuite (suiteName);
suite->add (makeTestGadget (testNamePrefix + "NW", fixture, apply (testFunc, -1, -1)));
suite->add (makeTestGadget (testNamePrefix + "N", fixture, apply (testFunc, 0, -1)));
suite->add (makeTestGadget (testNamePrefix + "NE", fixture, apply (testFunc, 1, -1)));
suite->add (makeTestGadget (testNamePrefix + "W", fixture, apply (testFunc, -1, 0)));
suite->add (makeTestGadget (testNamePrefix + "C", fixture, apply (testFunc, 0, 0)));
suite->add (makeTestGadget (testNamePrefix + "E", fixture, apply (testFunc, 1, 0)));
suite->add (makeTestGadget (testNamePrefix + "SW", fixture, apply (testFunc, -1, 1)));
suite->add (makeTestGadget (testNamePrefix + "S", fixture, apply (testFunc, 0, 1)));
suite->add (makeTestGadget (testNamePrefix + "SE", fixture, apply (testFunc, 1, 1)));
return suite;
}
The main difference is that the set-up of the central rectangle, the test rectangle and the
expected result computation is moved to some virtual (!) helper functions. Also a static
function was added to create a test suite.
Now it is quite easy to create completely different test suites with a few lines of code.
I.e. if we want to test if the intersection and the coverage test function produce correct
results when they must handle cases in which rectangles slightly overlap, this could be
done in a systematic way like this: A new fixture class is derived that changes some
configuration parameters (boundaries of the test rectangles) and by using such derived fixture
objects when creating test suites.
class GrownRectTester : public BasicRectTester {
protected:
virtual void setUpTestRect (Rect& r, int cx, int cy) {
r.reshape (cx * 10 - 2, cy * 10 - 2, 10 + 4, 10 + 4);
}
virtual bool expectedIntersectsResult (int cx, int cy) {
return true;
}
virtual bool expectedCoversResult (int cx, int cy) {
return false;
}
};
Setting up two different test suites now take only a few lines of code:
BasicRectTester* basicRectFixture = new BasicRectTester ();
GrownRectTester* grownRectFixture = new GrownRectTester ();
manager.takeFixture (basicRectFixture);
manager.takeFixture (grownRectFixture);
top->add (BasicRectTester::makePredicateTests (
"Basic intersection test", "Basic Intersection ",
&BasicRectTester::testIntersects, basicRectFixture));
top->add (BasicRectTester::makePredicateTests (
"Grown rect intersection test", "Overlapping Rect Instersection ",
&BasicRectTester::testIntersects, grownRectFixture));
top->add (BasicRectTester::makePredicateTests (
"Basic covrage test", "Basic Coverage ",
&BasicRectTester::testCovers, basicRectFixture));
top->add (BasicRectTester::makePredicateTests (
"Grown rect coverage test", "Overlapping Rect Coverage ",
&BasicRectTester::testCovers, grownRectFixture));
Now you can see the full power of fixture objects. With two fixture classes and some lines for
generating the test suites 38 distinct (!) tests are generated that test the intersection and coverage test
functions systematically under all constellations.
Please note that both fixture instances, basicRectFixture and grownRectFixture
are handed over to the test manager by invoking takeFixture. After calling this function,
the fixtures are owned by the test manager, so they are deleted when the manager is deleted.
This prevents us from worrying about which test case that uses the fixture object actually
owns the test case.
This might be no problem if the number of (auto-generated) test cases is small; but if the set
of test cases is huge or varies over time, it is better to delegate the deletion of a shared
fixture object to the test manager.
The Scrunitizer C++ Unit Test Framework
by Bernd Linowski
[Scrunitizer]
[Overview]
[Cookbook]
[Download]
[Index]
[Linosphere]
Page generated: 1 Nov 2000
(C) by Bernd Linowski
scrunitizer@linosphere.de