[Cppunit-devel] A new architecture ?
Brought to you by:
blep
|
From: Steve M. R. <ste...@vi...> - 2001-10-22 04:02:58
|
Hello,
So I spent some time yesterday thinking about the architecture of
CppUnit, basically the same issues that I raised previously in two
messages:
http://www.geocrawler.com/archives/3/6780/2001/7/0/6126116/
http://www.geocrawler.com/archives/3/6780/2001/7/0/6126118/
In those messages, I tried to outline my understanding of the various
classes in the current CppUnit. In the end, I concluded that a lot of
my difficulty in understanding CppUnit came from a couple of rather
odd bits of system architecture. As one example, there is the class
TestCase that is used in two different ways: it can be used to
implement a single test case, or it can be used as a "fixture" to
implement several test cases. In addition, there is the fact that a
test case can be programmed to check for an "expected" exception if
the case is constructed with TestCaller, but not if it is a direct
subclass of TestCase. And there is the truly baroque TestResult class
that is very inflexible and heavy.
I spent some time considering what a clean design would look like, and
then some more time actually coding it. There are two large groups of
classes needed for unit testing: classes to set up the tests, and
classes to run them and capture the output. I thought about putting
these two classes into separate namespaces, which I will tentatively
call CppUnit::Test and CppUnit::Result.
Test Construction Classes (CppUnit::Test)
-----------------------------------------
I'll leave aside the assertions and the exception classes for
the moment.
The current design includes single test cases and suites of test
cases, both of which implement a common interface. As Baptiste
pointed out these are, respectively, the leaf, composite, and
component classes of a "Composite Pattern". I propose to have three
classes that serve the same purposes, all in CppUnit::Test
namespace:
Base - a pure virtual class that defines the interface (i.e. the Component)
Case - the leaf class that implements a *single* test case; subclass of Base
Suite - the composite component (also a subclass of Base), consisting
of a set of tests where a test could be any subclass of Base
To these basic three -- each of which are fairly simple and clean -- \
I also added the following.
CaseWithException: a single test that requires that a specified
exception is thrown. The class is templated on the exception
type.
FunctionAdaptor: takes a pointer to a "void foo(void)" function
and turns it into a CppUnit::Test::Case object.
MethodAdaptor: takes a pointer to a "void foo(void)" class method
and turns it into a CppUnit::Test::Case object.
These three are all subclasses of CppUnit::Test::Case, so very little
extra code is required.
Using the MethodAdaptor and CaseWithException, I can easily implement
the CPPUNIT_TEST_SUITE(), CPPUNIT_TEST(), CPPUNIT_TEST_EXCEPTION(),
and CPPUNIT_TEST_SUITE_END() macros. The beauty of this scheme is
that *any* class can contain a test suite. In the current scheme, I
think that you can only use these macros in a subclass of
CppUnit::TestCase. And with the FunctionAdaptor you can make test
suites out of arbitrary global functions.
So the test-definition side seems sufficiently flexible, at least
to me :-). Have I missed something out?
Test Result Classes (CppUnit::Result)
-------------------------------------
To keep the code clean and flexible, I opted here to use the composite
pattern again. I used CppUnit::Result::Base for the interface class,
class Base
{
public:
virtual ~Base() {};
//! Callback to indicate a test is about to start.
virtual void startTest( CppUnit::Test::Base* test ) = 0;
//! Callback to indicate that a failure has occurred.
virtual void addFailure( CppUnit::Failure* failure ) = 0;
//! Callback to indicate a test has just ended.
virtual void endTest( CppUnit::Test::Base* test ) = 0;
};
and CppUnit::Result::Group for the composite class. The latter is
basically identical to the test suite class in that it contains
a vector of pointers to result classes, and the callbacks are
passed along to each result object in the group.
There are a number of possible leaf classes. The simplest ones
will just print the test progress to the screen. For example,
here is one in the same vein as the CompilerOutputter classes
recently added. This one is for emacs-compatible output.
class EmacsOutput : public CppUnit::Result::Base
{
public:
void startTest( CppUnit::Test::Base* test ) {}
void addFailure( CppUnit::Failure* failure )
{
if ( failure->getSourceLine().isValid() ) {
std::cerr << failure->getSourceLine().fileName()
<< ":"
<< failure->getSourceLine().lineNumber()
<< ":";
} else {
std::cerr << "Error at unknown location:";
}
std::cerr << failure->getMessage()
<< std::endl;
};
void endTest( CppUnit::Test::Base* test )
{}
};
It cannot get any simpler than this! Output for other IDEs will be
similarly trivial. If you have a GUI running, then you can implement
a leaf leaf class that updates the progress meter.
If you want to save the failures and generate a report at the
end, then you just implement another leaf class that stores
a copy of the failures received.
What's Next
-----------
I believe that the current functionality is available using the simple
classes outlined above. Those, plus the current assertion functions,
exception class, and test registry classes. I haven't looked too
carefully at the latter, however; perhaps there is room to simplify
those also.
There are two questions to consider. First, is it worth reworking the
foundations of CppUnit? Certainly, the gain in clarity is persuasive
to me; how about the rest of you? Second, if we decide to go ahead
with this, how much compability must be maintained?
I have implemented the classes mentioned above along with the
CPPUNIT_TEST_SUITE() macros, and was able to use the macro-based test
suites previously written with no modifications. Since none of the
assertion macros had changed, my custom assertion_traits worked okay.
I did have to change my custom TestResult class, but since it was
customized precisely to put the output in emacs format, I can happily
change to the EmacsOutput class. ;-)
My initial ambition was to reimplement completely the current Test,
TestCase, TestCaller, TestResult classes using the new classes. I
think that is a lot work and I'm not sure whether the gain is worth
it. It is straightforward to replace the TEST_SUITE macros with
equivalent ones using the new classes. So any tests that use the
macros alone will continue to function. What other techniques are in
use? Derivation of TestCase? Direct use of TestCaller and TestCase
subclass methods? How many are worth supporting?
At the moment, none of the code is in CVS. There are two reasons for
this. First, I'd like to see how people feel about pursuing this.
Second, I made a mistake in choosing the namespace CppUnit::Test.
Using that namespace will prevent it from coexisting with the old
CppUnit::Test class. Since I'd like, as much as possible, to
reimplement the old classes in terms of the new I'm going to change
the new classes to a different namespace. Perhaps CppUnit::Setup ?
Suggestions welcomed.
-Steve
--
by Rocket to the Moon,
by Airplane to the Rocket,
by Taxi to the Airport,
by Frontdoor to the Taxi,
by throwing back the blanket and laying down the legs ...
- They Might Be Giants
|