Libin 的个人资料绿色家园照片日志列表 工具 帮助
12月22日

Exploring the C++ Unit Testing Framework Jungle

By Noel Llopis
28 December 2004
 
One of the topics I've been meaning to get to for quite a while is the applicability of test-driven development in games. Every time the topic comes up in conversations or mailing lists, everybody is always very curious about it and they immediately want to know more. I will get to that soon. I promise!

In the meanwhile I'm now in the situation that I need to choose a unit-testing framework to roll out for my team at work. So, before I get to talk about how to use test-driven development in games, or the value of unit testing, or anything like that, we dive deep into a detailed comparison of existing C++ unit-testing frameworks. Hang on tight. It's going to be a long and bumpy ride with a plot twist at the end.

If you just want to read about a specific framework, you can go directly there:

Overview

How do we choose a unit-testing framework? It depends on what we're going to do with it and how we're going to use it. If I used Java for most of my work, the choice would be easy since JUnit seems to be the framework of choice for those working with Java. I don't hear them arguing over frameworks or proposing new ones very frequently, so it must be pretty good.

Unfortunately that's not the case with C++. We have our XUnit family member, CppUnit, but we're clearly not happy with that. We have more unit-testing frameworks than you can shake a stick at. And a lot of teams end up writing their own from scratch. Why is that? Is C++ so inadequate for unit testing that we have trouble fitting the XUnit approach in the language? Not like it's a bad thing, mind you. Diversity is good. Otherwise I would be stuck writing this under Windows and you would be stuck reading it with Internet Explorer. In any case, I'm clearly not the first one who's asked this question. This page tries to answer the question, and comes up with some very plausible answers: differences in compilers, platforms, and programming styles. C++ is not exactly a clean, fully supported language, with one coding standard.

A good way to start is to create a list of features that are important given the type of work I expect to be doing. In particular, I want to be doing test-driven development (TDD), which means I'm going to be constantly writing and running many small tests. It's going to be used for game development, so I'd like to run the tests in a variety of different platforms (PC, Xbox, PS2, next-generation consoles, etc). It should also fit my own personal TDD style (many tests, heavy use of fixtures, etc).

The following list summarizes the features I would like in a unit-testing framework in order of importance. I'll evaluate each framework on the basis of these features. Thanks to Tom Plunket for providing a slightly different view on the topic that helped me to re-evaluate the relative importance of the different features.

  1. Minimal amount of work needed to add new tests. I'm going to be doing this all the time, so I don't want to do a lot of typing, and I especially don't want to do any duplicated typing. The shorter and easier it is to write, the easier it'll be to refactor, which is crucial when you're doing TDD.

  2. Easy to modify and port. It should have no dependencies with non-standard libraries, and it shouldn't rely on "exotic" C++ features if possible (RTTI, exception handling, etc). Some of the compilers we have to use for console development are not exactly cutting edge. To verify this one, I created a set of unit tests using each library under Linux with g++. Since most of the tests are written with Windows and Visual Studio in mind, it's not a bad initial test.

  3. Supports setup/teardown steps (fixtures). I've adopted the style recommended by David Astels in his book Test Driven Development: A Practical Guide about using only one assertion per test. It really makes tests a lot easier to understand and maintain, but it requires heavy use of fixtures. A framework without them is ruled out immediately. Bonus points for frameworks that let me declare objects used in the fixture on the stack (and still get created right before the test) as opposed to having to allocate them dynamically.

  4. Handles exceptions and crashes well. We don't want the tests to stop just because some code that was executed accessed some invalid memory location or had a division by zero. The unit-testing framework should report the exception and as much information about it as possible. It should also be possible to run it again and have the debugger break at the place where the exception was triggered.

  5. Good assert functionality. Failing assert statements should print the content of the variables that were compared. It should also provide a good set of assert statements for doing "almost equality" (absolutely necessary for floats), less than, more than, etc. Bonus points for providing ways to check whether exceptions were or were not thrown.

  6. Supports different outputs. By default, I'd like to have a format that can be understood and parsed by IDEs like Visual Studio or KDevelop, so it's easy to navigate to any test failures as if they were syntax errors. But I'd also like to have ways to display different outputs (more detailed ones, shorter ones, parsing-friendly ones, etc).

  7. Supports suites. It's kind of funny that this is so low in my priority list when it's usually listed as a prominent feature in most frameworks. Frankly, I've had very little need for this in the past. It's nice, yes, but I end up having many libraries, each of them with its own set of tests, so I hardly ever need this. Still, it certainly would be nice to have around in case it starts getting slow to run the unit tests at some point.

Bonus: Timing support. Both for total running time of tests, and for individual ones. I like to keep an eye on my running times. Not for performance reasons, but to prevent them from getting out of hand. I prefer to keep running time to under 3-4 seconds (it's the only way to be able to run them very frequently). Ideally, I'd also like to see a warning printed if any single test goes over a certain amount of time.

Easy of installation was not considered a priority; after all, I only have to go through that once—it's creating new tests that I'm going to be doing all day long. Non-commercial-friendly licenses (like GPL or LGPL) are also not much of an issue because the unit test framework is not something we're going to link to the executable we ship, so they don't really impose any restrictions on the final product.

Incidentally, during my research for this article, I found that other people have compiled lists of what they wish for in C++ unit-testing frameworks . It's interesting to contrast that article with this one and make a note of the differences and similarities between what we'd like to see in a unit test framework.

Ideal Framework

Before I start going over each of the major (and a few minor) C++ unit-testing frameworks, I decided I would apply the philosophy behind test-driven development to this analysis and start by thinking what I would like to have. So I decided to write the set of sample tests in some ideal unit-testing framework without regard for language constrains or anything. In the ideal world, this is what I would like my unit tests to be like.

The simplest possible test should be trivial to create. Just one line to declare the test and then the test body itself:

TEST (SimplestTest)
{
    float someNum = 2.00001f;
    ASSERT_CLOSE (someNum, 2.0f);
}

A test with fixtures is going to be a bit more complicated, but it should still be really easy to set up:

SETUP (FixtureTestSuite)
{
    float someNum = 2.0f;
    std::string str = "Hello";
    MyClass someObject("somename");
    someObject.doSomethng();
}

TEARDOWN (FixtureTestSuite)
{
    someObject.doSomethingElse();
}

TEST (FixtureTestSuite, Test1)
{
    ASSERT_CLOSE (someNum, 2.0f);
    someNum = 0.0f;
}

TEST (FixtureTestSuite, Test2)
{
    ASSERT_CLOSE (someNum, 2.0f);
}

TEST (FixtureTestSuite, Test3)
{
    ASSERT_EQUAL(str, "Hello");
}

The first thing to point out about this set of tests is that there is a minimum amount of code spent in anything other than the tests themselves. The simplest possible test takes a couple of lines and needs no support other than a main file that runs all the tests. Setting up a fixture with setup/teardown calls should also be totally trivial. I don't want to inherit from any classes, override any functions, or anything. Just write the setup step and move on.

Look at the setup function again. The variables that are going to be used in the tests are not dynamically created. Instead, they appear to be declared on the stack and used directly there. Additionally, I should point out that those objects should only be created right before each test, and not before all tests start. How exactly are the tests going to use them? I don't know, but that's what I would like to use. That's why this is an ideal framework.

Now let's contrast it to six real unit-testing frameworks that have to worry about actually compiling and running. For each of the frameworks I look at the list of wanted features and I try to implement the two tests I implemented with this ideal framework. Here is the source code for all the examples.

CppUnit

CppUnit is probably the most widely used unit-testing framework in C++, so it's going to be a good reference to compare other unit tests against. I had used CppUnit three or four of years ago and my impressions back then were less than favorable. I remember the code being a mess laced with MFC, the examples all tangled up with the framework, and the silly GUI bar tightly coupled with the software. I even ended up creating a patch to provide console-only output and removed MFC dependencies. So this time I approached it with a bit of apprehension to say the least.

I have to admit that CppUnit has come a long way since then. I was expecting the worst, but this time I found it much easier to use and configure. It's still not perfect, but it's much, much better than it used to. The documentation is pretty decent, but you'll have to end up digging deep into the module descriptions to even find out that some functionality is available.

  1. Minimal amount of work needed to add new tests. This is one of the major downfalls of CppUnit, and, ironically, it's the highest-rated feature I was looking for. CppUnit requires quite a bit of work for the simplest possible test.

// Simplest possible test with CppUnit
#include <cppunit/extensions/HelperMacros.h>

class SimplestCase : public CPPUNIT_NS::TestFixture
{
  CPPUNIT_TEST_SUITE( SimplestCase );
  CPPUNIT_TEST( MyTest );
  CPPUNIT_TEST_SUITE_END();

protected:
  void MyTest();
};

CPPUNIT_TEST_SUITE_REGISTRATION( SimplestCase );

void SimplestCase::MyTest()
{
    float fnum = 2.00001f;
    CPPUNIT_ASSERT_DOUBLES_EQUAL( fnum, 2.0f, 0.0005 );
}
  1. Easy to modify and port. It gets mixed marks on this one. On one hand, it runs under Windows and Linux, and the functionality is reasonably well modularized (results, runners, outputs, etc). On the other hand, CppUnit still requires RTTI, the STL, and (I think) exception handling. It's not the end of the world to require that, but it could be problematic if you want to link against libraries that have no RTTI enabled, or if you don't want to pull in the STL.

  2. Supports fixtures. Yes. If you want the objects to be created before each test, they need to be dynamically allocated in the setup() function though, so no bonus there.

#include <cppunit/extensions/HelperMacros.h>
#include "MyTestClass.h"


class FixtureTest : public CPPUNIT_NS::TestFixture
{
  CPPUNIT_TEST_SUITE( FixtureTest );
  CPPUNIT_TEST( Test1 );
  CPPUNIT_TEST( Test2 );
  CPPUNIT_TEST( Test3 );
  CPPUNIT_TEST_SUITE_END();

protected:
  float someValue;
  std::string str;
  MyTestClass myObject;

public:
  void setUp();

protected:
  void Test1();
  void Test2();
  void Test3();
};


CPPUNIT_TEST_SUITE_REGISTRATION( FixtureTest );

void FixtureTest::setUp()
{
  someValue = 2.0;
  str = "Hello";
}

void FixtureTest::Test1()
{
  CPPUNIT_ASSERT_DOUBLES_EQUAL( someValue, 2.0f, 0.005f );
  someValue = 0;

  //System exceptions cause CppUnit to stop dead on its tracks
  //myObject.UseBadPointer();

  // A regular exception works nicely though
  myObject.ThrowException();
}


void FixtureTest::Test2()
{
  CPPUNIT_ASSERT_DOUBLES_EQUAL( someValue, 2.0f, 0.005f );
  CPPUNIT_ASSERT_EQUAL (str, std::string("Hello"));
}

void FixtureTest::Test3()
{
    // This also causes it to stop completely
    //myObject.DivideByZero();

    // Unfortunately, it looks like the framework creates 3 instances of MyTestClass
    // right at the beginning instead of doing it on demand for each test. We would
    // have to do it dynamically in the setup/teardown steps ourselves.
    CPPUNIT_ASSERT_EQUAL (1, myObject.s_currentInstances);
    CPPUNIT_ASSERT_EQUAL (3, myObject.s_instancesCreated);
    CPPUNIT_ASSERT_EQUAL (1, myObject.s_maxSimultaneousInstances);
}
  1. Handles exceptions and crashes well. Yes. It uses the concept of "protectors" which are wrappers around tests. The default one attempts to catch all exceptions (and identify some of them). You can write your own custom protectors and push them on the stack to combine them with the ones already there. It didn't catch system exceptions under Linux, but it would have been trivial to add with a new protector. I don't think it had a way to easily turn off exception handling and let the debugger break where the exception happened though (no define or command-line parameter).

  2. Good assert functionality. Pretty decent. It has the minimum set of of assert statements, including one for comparing floating-point numbers. It's missing asserts for less than, greater than, etc. The contents of the variables compared are printed to a stream if the assert fails, giving you as much information as possible about the failed test.

  3. Supports different outputs. Yes. Has very-well defined functionality for "outputters" (which display the results of the tests), as well as "listeners" (which get notified while the tests are happening). It comes with an IDE-friendly output that is perfect for integrating with Visual Studio. Also supports GUI progress bars and the like.

  4. Supports suites. Yes.

Overall, CppUnit is frustrating because it's almost exactly what I want, except for my most wanted feature. I really can't believe that it takes so much typing (and duplicated typing at that) to add new tests. Other than that, the main complaint is the need for RTTI or exceptions, and the relative complexity of the source code, which could make it challenging to port to different platforms.

Boost.Test

Update: I've revised my comments and ratings of the Boost.Test framework in light of the comments from Gennadiy Rozental pointing out how easy it is to add fixtures in boost.

I'm a big fan of Boost, but I have to admit that it wasn't until about a year ago that I even learned that Boost was providing a unit testing library. Clearly, I had to check it out.

The first surprise is that Boost.Test isn't exclusively a unit-testing framework. It also pretends to be a bunch of other things related to testing. Nothing terribly wrong with that, but to me is the first sign of a "smell." The other surprise is that it wasn't really based on the XUnit family. Hmmm... In that case, it had better provide some outstanding functionality.

The documentation was top notch. Some of the best I saw for any testing framework. The concepts were clearly explained, and had lots of simple examples to demonstrate different features. Interestingly, from the docs I saw that Boost.Test was designed to support some things that I would consider bad practices such as dependencies between tests, or long tests.

  1. Minimal amount of work needed to add new tests. Almost. Boost.Test requires really minimal work to add new tests. It's very much like the ideal testing framework described earlier. Unfortunately, adding tests that are part of a suite requires more typing and explicit registration of each test.

#include <boost/test/auto_unit_test.hpp>
#include <boost/test/floating_point_comparison.hpp>

BOOST_AUTO_UNIT_TEST (MyFirstTest)
{
        float fnum = 2.00001f;
    BOOST_CHECK_CLOSE(fnum, 2.f, 1e-3);
}
  1. Easy to modify and port. It gets mixed marks on this one, for the same reasons as CppUnit. Being part of the Boost libraries, portability is something that they take very seriously. It worked flawlessly under Linux (better than most frameworks). But I question how easy it is to actually get inside the source code and start making modifications. It also happens to pull into quite a few supporting headers from other Boost libraries, so it's not exactly small and self-contained.

  2. Supports fixtures. Boost.Test eschews the setup/teardown structure of NUnit tests in favor of plain C++ constructors/destructors. At first this threw me off for a loop. After years of being used to setup/teardown, and a fairly complex suite setup, I didn't see the obvious ways of using fixtures with composition.

    Now that I've tried it this way I've come to like it almost better than setup/teardown fixtures. One of the great advantages of this approach is that you don't need to create fixture objects dynamically, and instead you can put the whole fixture on the stack.

    On the downside, it's annoying to have to refer to the variables in the fixture through the object name. It would be great if they could somehow magically appear in the same scope as the test case itself. Also, it would have been a bit cleaner if the fixture could have been setup on the stack by the BOOST_AUTO_UNIT_TEST macro instead of having to explicitely put it on the stack for every test case.

#include <boost/test/auto_unit_test.hpp>
#include <boost/test/floating_point_comparison.hpp>
#include "MyTestClass.h"

struct MyFixture
{
    MyFixture() 
    {
        someValue = 2.0;
        str = "Hello";
    }

    float someValue;
    std::string str;
    MyTestClass myObject;   
};


BOOST_AUTO_UNIT_TEST (TestCase1)
{
    MyFixture f;    
    BOOST_CHECK_CLOSE (f.someValue, 2.0f, 0.005f);
    f.someValue = 13;
}

BOOST_AUTO_UNIT_TEST (TestCase2)
{
    MyFixture f;   
    BOOST_CHECK_EQUAL (f.str, std::string("Hello"));
    BOOST_CHECK_CLOSE (f.someValue, 2.0f, 0.005f);

    // Boost deals with this OK and reports the problem        
    //f.myObject.UseBadPointer();
    // Same with this
    //myObject.DivideByZero();
}

BOOST_AUTO_UNIT_TEST (TestCase3)
{
    MyFixture f;
    BOOST_CHECK_EQUAL (1, f.myObject.s_currentInstances);
    BOOST_CHECK_EQUAL (3, f.myObject.s_instancesCreated);
    BOOST_CHECK_EQUAL (1, f.myObject.s_maxSimultaneousInstances);
}
  1. Handles exceptions and crashes well. This is one of the aspects where Boost.Test is head and shoulders above all the competition. Not only does it handle exceptions correctly, but it prints some information about them, it catches Linux system exceptions, and it even has a command-line argument that disables exception handling, which allows you to catch the problem in your debugger on a second run. I really couldn't ask for much more.

  2. Good assert functionality. Yes. Has assert statements for just about any operation you want (equality, closeness, less than, greater than, bitwise equal, etc). It even has support for checking whether exceptions were thrown. The assert statements correctly print out the contents of the variables being checked. Top marks on this one.

  3. Supports different outputs. Probably, but it's not exactly trivial to change. At least the default output is IDE friendly. I suspect I would need to dig deeper into the unit_test_log_formatter, but I certainly didn't see a variety of preset output types that I could just plug in.

  4. Supports suites. Yes, but with a big catch. Unless I'm missing something (which is very possible at this point--if so make sure to let me know), creating a suite requires a bunch of fairly verbose statements and also requires modifying the test runner itself in main. Have a look at the example below. Couldn't that have been simplified to the extreme? It's not a big deal as this is my least-wanted requirement, but I wish I could label all the test cases in one file as part of a suite with a simple macro at the beginning of the file. Another minor shortcoming is the lack of setup/teardown steps for whole suites, which could come in really handy (especially if suite creation were streamlined).

#include <boost/test/unit_test.hpp>
#include <boost/test/floating_point_comparison.hpp>
using boost::unit_test::test_suite;


struct MyTest 
{
    void TestCase1()
    {
    	float fnum = 2.00001f;
        BOOST_CHECK_CLOSE(fnum, 2.f, 1e-3);
    }
    
    void TestCase2()
    {
    }
};

test_suite * GetSuite1() 
{
    test_suite * suite  = BOOST_TEST_SUITE("my_test_suite");
    
    boost::shared_ptr instance( new MyTest() );
    suite->add (BOOST_CLASS_TEST_CASE( &MyTest::TestCase1, instance ));
    suite->add (BOOST_CLASS_TEST_CASE( &MyTest::TestCase2, instance ));

    return suite;
}
#include <boost/test/auto_unit_test.hpp>
using boost::unit_test::test_suite;
extern test_suite * GetSuite1();

boost::unit_test::test_suite*
init_unit_test_suite( int /* argc */, char* /* argv */ [] ) {
    test_suite * test = BOOST_TEST_SUITE("Master test suite");
    test->add( boost::unit_test::ut_detail::auto_unit_test_suite() );
    test->add(GetSuite1());
    return test;
}

Boost.Test is a library with a huge amount of potential. It has great support for exception handling and advanced assert statements. It also has other fairly unique functionality such as support for checking for infinite loops, and different levels of logging. On the other hand, it's very verbose to add new tests that are part of a suite, and it might be a bit heavy weight for game console environments.

CppUnitLite

CppUnitLite has a funny story behind it. Michael Feathers, the original author of CppUnit, got fed up with the complexity of CppUnit and how it didn't fit everyone's needs, so we wrote the ultra-light weight framework CppUnitLite. It is as light on the features as it is on complexity and size, but his philosophy was to let people customize it to deal with whatever they need.

Indeed, CppUnitLite is only a handful of files and it probably adds up to about 200 lines of very clear, easy to understand and modify code. To be fair, in this comparison I actually used a version of CppUnitLite I modified a couple of years ago (download it along with all the sample code) to add some features I needed (fixtures, exception handling, different outputs). I figured it was definitely in the spirit that CppUnitLite was intended, and if nothing else, it can show what can be accomplished by just a few minutes of work with the source code.

On the other hand, CppUnitLite doesn't have any documentation to speak of. Heck, it doesn't even have a web site of its own, which I'm sure is not helping the adoption rate by other developers.

  1. Minimal amount of work needed to add new tests. Absolutely! Of all the unit-test frameworks, this is the one that comes closest to the ideal. On the other hand, it could be the fact that I've used CppUnitLite the most and I'm biased. In any way, it really fits my idea of minimum amount of work required to set up a simple test or even one with a fixture (although that could be made even better).

#include "lib/TestHarness.h"

TEST (Whatever,MyTest)
{
    float fnum = 2.00001f;
    CHECK_DOUBLES_EQUAL (fnum, 2.0f);
}
  1. Easy to modify and port. Definitely. Again, it gets best of the class award in this category. No other unit-test framework comes close to being this simple, easy to modify and port, and at the same time having reasonably well separated functionality. The original version of CppUnitLite even had a special lightweight string class to avoid dependencies on STL. In my modified version I changed it to use std::string since that's what I use in most of my projects, but the change took under one minute to do. Also, using it under Linux was absolutely trivial, even though I had only used it under Windows before.

  2. Supports fixtures. This is where the original CppUnitLite starts running into trouble. It's so lightweight that it doesn't have room for many features. This was an absolute must for me, so I went ahead and added it. I'm sure it could be improved to make it so adding a fixture requires even less typing, but it's functional as it stands. Unfortunately, it suffers from the problem that objects need to be created dynamically if we want them to be created right before each test. To be fair though, every single unit-test framework in this evaluation has that requirement. Oh well.

#include "lib/TestHarness.h"
#include "MyTestClass.h"


class MyFixtureSetup : public TestSetup
{
public:
    void setup()
    {
        someValue = 2.0;
        str = "Hello";
    }

    void teardown()
    {
    }

protected:
    float someValue;
    std::string str;
    MyTestClass myObject;
};


TESTWITHSETUP (MyFixture,Test1)
{
    CHECK_DOUBLES_EQUAL (someValue, 2.0f);
    someValue = 0;

    // CppUnitLite doesn't handle system exceptions very well either
    //myObject.UseBadPointer();

    // A regular exception works nicely though
    myObject.ThrowException();
}

TESTWITHSETUP (MyFixture,Test2)
{
    CHECK_DOUBLES_EQUAL (someValue, 2.0f);
    CHECK_STRINGS_EQUAL (str, std::string("Hello"));
}


TESTWITHSETUP (MyFixture,Test3)
{
    // Unfortunately, it looks like the framework creates 3 instances of MyTestClass
    // right at the beginning instead of doing it on demand for each test. We would
    // have to do it dynamically in the setup/teardown steps ourselves.
    CHECK_LONGS_EQUAL (1, myObject.s_currentInstances);
    CHECK_LONGS_EQUAL (3, myObject.s_instancesCreated);
    CHECK_LONGS_EQUAL (1, myObject.s_maxSimultaneousInstances);
}
  1. Handles exceptions and crashes well. The original CppUnitLite didn't handle them at all. I added minor support for this (just an optional try/catch). To run the tests without exception support it requires recompiling the tests with a special define turned on, so it's not at slick as the command-line argument that Boost.Test features.

  2. Good assert functionality. Here is where CppUnitLite really shows its age. The assert macros are definitely the worst of the lot. They don't use a stream to print out the contents of their variables, so we need custom macros for each object type you want to use. It comes with support for doubles, longs, and strings, but anything else you need to add by hand. Also, it doesn't have any checks for anything other than equality (or closeness in the case of floating-point numbers).

  3. Supports different outputs. Again, the original only had one type of output. But it was very well isolated and it was trivial to add more.

  4. Supports suites. Probably the only framework that doesn't support suites. I never really needed them, but they would probably be very easy to add on a per-file basis.

CppUnitLite is as barebones as it gets, but with a few modifications it hits the mark in all the important categories. If it had better support for assert statements, it would come very close to my ideal framework. Still, it's a worthy candidate for the final crown.

NanoCppUnit

I had never heard of NanoCppUnit until Phlip brought it up. From reading the feature list, it really appeared to be everything that I wanted CppUnitLite to be, except that it was better and ready to work out of the box.

The first point against NanoCppUnit is the awful "packaging" of the framework. If you thought that CppUnitLite was bad (not having a web page of its own), well, at least you could download it as a zip file. For NanoCppUnit you actually have to copy and paste the five files that make up the framework from a web page. I'm not kidding. That makes for some "lovely" formatting issues I might add. The documentation found in the web page wasn't exactly very useful either.

In any case, I continued my quest to get a simple test program up and running with NanoCppUnit. Out of the box (or out of the web page, rather) it's clearly aimed only at Windows platforms. I thought it would be trivial to fix, but changing it required more time than I thought at first (I personally gave up when I started getting errors buried three macros deep into some assert statement). Unlike CppUnitLite, the source code is not very well structured at all, full of ugly macros everywhere, making it not trivial to add new features like new output types. Unless I'm totally mistaken, it even looks like it has sample code inside the test framework itself. Eventually I had to give up on running it under Linux, so my comments here are just best guesses by looking at the source code.

  1. Minimal amount of work needed to add new tests. I think so. I'm not sure it's possible to create a standalone test that is part of a global suite, but at least creating a suite doesn't require manual registration of every test. This is (probably) the simplest possible test with NanoCppUnit.

struct MySuite:  TestCase
{
};

TEST_(MySuite, MyTest)
{
    float fnum = 2.00001f;
    CPPUNIT_ASSERT_DOUBLES_EQUAL(fnum, 2.0f, 0.0001);
}
  1. Easy to modify and port. Not really. Windows dependencies run deeper than it seems on the surface. The code is small, but it's messy enough that it's a pain to work with. I'm sure it can be ported with a bit of effort though since it's so small.

  2. Supports fixtures. Yes. Setup and teardown calls very similar to the modified version of CppUnitLite.

  3. Handles exceptions and crashes well. No idea since I wasn't able to run it. I see some try/catch statements in the code, but no way to turn them on or off. Probably no better than CppUnitLite.

  4. Supports different outputs. Not really. Everything is hardwired to use a stream that sends its contents to OutputDebugString() in Windows. I think the default output text is formatted to match the Visual Studio error format.

  5. Good assert functionality. Yes. Good range of assert statements, including floating point closeness, greater than, less than, etc.

  6. Supports suites. Yes. I don't know what's involved in just running a single suite though. Not a big deal either way.

One of NanoCppUnit's unique features is regular expression support as part of its assert tests. That's very unusual, but I can see how it could come in handy. A few times in the past, I've had to check that a certain line of code has some particular format, so I had to sscanf it, and then check on some of the contents. A regular expression check would have done the job nicely.

Unfortunately, NanoCppUnit doesn't really live up to the standards of other frameworks. Right now it feels too much as a work in progress, with too much missing functionality and not clearly structured code.

Unit++

The further along we get in this evaluation, the less Xunit-like the frameworks become. Unit++'s unique feature is that it pretends to be more C++-like than CppUnit. Wait a second, did I hear that right? More C++ like? Is that supposed to be a good thing? Looking back at my ideal test framework, it really isn't very much like C++ at all. Once I started thinking about that topic I realized that there really is no reason at all why the tests framework itself needs to be in C++. The tests you write need to be in the language of the code being tested, but all the wrapper code doesn't. That's a point that the next, and final, testing framework will drive home.

So, what does it mean to be more C++ like? No macros for a start. You create suites of tests by creating classes that derive from suite. That's the same thing we were doing in most other frameworks, really, but it was just happening behind the scenes. It really doesn't help me any to know that that is what I'm doing, and I would certainly not call it a "feature." As a result, tests are more verbose than they could be.

The documentation is simply middle-of-the-road. It's there, but it's not particularly detailed and it doesn't come loaded with examples.

  1. Minimal amount of work needed to add new tests. I'm afraid it gets failing marks for this. It requires manual registration of tests, and every test needs to be part of a suite. This makes adding new tests tedious and error prone (by writing a new test and forgetting to register it). I don't know about you, but with all the C++ cruft, I look at the code below and it's not immediately obvious what it does until I've scanned it a couple of times. The signal to noise ratio is pretty poor.

#define __UNITPP
#include "unit++.h"
using namespace unitpp;

namespace {

        class Test : public suite {
            void test1()
            {
                float fnum = 2.00001f;
                assert_eq("Checking floats", fnum, 2.0f);
            }
        public:
            Test() : suite("MySuite")
            {
                add("test1", testcase(this, "Simplest test", &Test::test1));
                suite::main().add("demo", this);
            }
        };
        Test* theTest = new Test();
}
  1. Easy to modify and port. So-so. It needs the STL and it pulls in some stuff like iostreams (which I remember having distinct problems with when I was working with STLPort). On the other hand the source code is relatively small and self-contained so it's certainly doable to port and modify if you're willing to put in some time.

  2. Supports fixtures. Another framework that I just can't see how to do fixtures with. Like Boost.Test, it seems to think that using the constructor and destructor for each class is all you need. A quick search for fixture or setup or teardown in the documentation doesn't reveal anything. I don't know if I'm totally missing something or if other people just write very different tests from me. I suppose I could create a new class for every fixture I want, put the setup in the constructor and the teardown in the destructor and inherit from it for every test case (and somehow figure out how to create an instance of that class and use it for each test run). It's probably possible, but it's not exactly trivial, is it? Again, the lack of fixtures puts this framework out of the running.

  3. Handles exceptions and crashes well. Average. It manages to catch regular exceptions without crashing, but that's about it. No system exceptions in Linux. No way to turn it off for debugging.

  4. Supports different outputs. I couldn't figure out how to do it from the documentation. There is probably a way to do it since it even supports GUI functionality , but it's not obvious (and there are no examples). Besides, by this point, having failed points 1 and 3, I wasn't really motivated to spend a while learning the framework. Incidentally, this is one of the few frameworks whose default text output is not formatted correctly for IDEs like KDevelop.

  5. Good assert functionality. It scrapes by with the minimum in this department. It provides equality and condition checks, but that's it. It doesn't even provide a float version of assert to check for "close enough." At least it prints the contents of the variables to a stream correctly.

  6. Supports suites. Yes, like most of them.

Overall, Unit++ is not really a candidate. Perhaps it's because it's not intended for the type of testing I intend to use it for, but it doesn't offer anything new over other frameworks and it has a lot of drawbacks of its own. The lack of fixtures is simply unforgivable.

CxxTest

After looking into a framework that tried to be different from XUnit (Unit++), I wasn't particularly looking forward to evaluating possibly the wackiest one of them all, CxxTest. I had never heard of it until a few days ago, but I knew that it required using Perl along the way to generate some C++ code. My spider senses were tingling.

Boy was I wrong!! Within minutes of using CxxTest and reading through its great documentation (the best by far), I was completely convinced this was the way to go. This came as a complete surprise to me since I was ready to leave somewhat dissatisfied and pronounce a victor between CppUnit and CppUnitLite.

Let's start from the beginning. What's with the use of Perl and why is it different from CppUnit? Erez Volk, the author of CxxTest, had the unique insight that just because we're testing a C++ program, we don't need to rely on C++ for everything. Other languages, such as Java, are better suited to what we want to do in a unit-testing framework because they have good introspection (reflection) capabilities. C++ is quite lacking in that category, so we're forced to use kludges like manual registration of tests, ugly macros, etc. CxxTest gets around that by parsing our simple tests and generating a C++ test runner that calls directly into our tests. The result is simply brilliant. We get all the flexibility we need without the need for any ugly macros, exotic libraries, or fancy language features. As a matter of fact, CxxTest's requirements are as plain vanilla as you can get (other than being able to run Perl).

The code-generation step is also trivial to integrate into the regular build system. The wonderful documentation gives explicit step-by-step instructions on how to integrate it with make files, Visual Studio projects files, or Cons. Once you have it set up, you won't even remember there's anything out of the ordinary going on.

Let's see how it stacks up against the competition.

  1. Minimal amount of work needed to add new tests. Very good. It's almost as simple as the best of them. If I could nit-pick, I would have wished for an even simpler way to create tests without the need to declare the class explicitly. Since we're doing processing with a Perl script, there's no reason we couldn't have taken it a step beyond that and used a syntax even closer to my ideal test framework.

class SimplestTestSuite : public CxxTest::TestSuite
{
public:
   void testMyTest()
   {
                float fnum = 2.00001f;
                TS_ASSERT_DELTA (fnum, 2.0f, 0.0001f);
   }
};
  1. Easy to modify and port. CxxUnit requires the simplest set of language features (no RTTI, no exception handling, no template functions, etc). It also doesn't require any external libraries. It is also distributed simply as a set of header files, so there's no need to compile into a separate library or anything like that. Functionality is pretty well broken down and separated in the original source code, so making modifications should be fairly straightforward.

  2. Supports fixtures. CxxUnit gets the "top of its class" label in this category. Not only does it support setup/teardown steps on a per-test level, but it also supports them at the suite and at the world (global) level. Creating fixtures is pretty straightforward and just requires inheriting from a class and creating as many functions as you want starting with the letters "test." To be really picky, I would have loved it if they had taken it a step further and, apart from simplifying the code a bit more, also inserted the setup and teardown code around the code for each test. That would have allowed us to work with those objects directly on the stack and their lifetime would have been managed correctly around each test. Oh well. Can't have everything.

#include "MyTestClass.h"

class FixtureSuite : public CxxTest::TestSuite
{
public:
   void setUp()
   {
       someValue = 2.0;
       str = "Hello";
   }
   void tearDown()
   {
   }

   void test1()
   {
        TS_ASSERT_DELTA (someValue, 2.0f, 0.0001f);
        someValue = 13.0f;

        // A regular exception works nicely though
        myObject.ThrowException();
   }
   void test2()
   {
        TS_ASSERT_DELTA (someValue, 2.0f, 0.0001f);
        TS_ASSERT_EQUALS (str, std::string("Hello"));
   }
   void test3()
   {
       //myObject.UseBadPointer();
        TS_ASSERT_EQUALS (1, myObject.s_currentInstances);
        TS_ASSERT_EQUALS (3, myObject.s_instancesCreated);
        TS_ASSERT_EQUALS (1, myObject.s_maxSimultaneousInstances);
   }

    float someValue;
    std::string str;
    MyTestClass myObject;
};
  1. Handles exceptions and crashes well. Great support. It catches all exceptions and prints information about them formatted like any other error (no system exceptions under Linux though). You can easily re-run the tests with a command-line argument to the Perl script to avoid catching exceptions and catch them in the debugger instead. It also gives you a custom version of every assert macro that lets you catch the exceptions yourself in case you ever need to do that.

  2. Supports different outputs. Different outputs are supported by passing a parameter indicating which type of output you want to the Perl processing step. The default one (error-printer) was formatted correctly for IDE parsing, and you can use several others (including GUIs for those of you addicted to progress bars, a yes/no report, or a stdio one). Adding new output formatting sounds very straightforward and it's even covered in the documentation.

  3. Good assert functionality. Again, it gets "top of its class" for this one. It has a whole suite of very comprehensive assert functions, including ones for exception handling, checking predicates, and arbitrary relations. It even has a way to print out warnings which can be used to differentiate between two parts of the code calling the same test, or to print reminder "TODO" messages to yourself.

  4. Supports suites. Yes. All tests are part of a suite.

Another feature supported by CxxUnit that I haven't had time to look into is some support for mock objects. Anybody doing TDD knows the value of mock objects when it comes to testing the interactions between a set of objects. Apparently CxxUnit allows you to override global functions with specific mock functions (it gives an example of overriding fopen()). I don't think it helps any with regular classes; for those you're on your own.

So, what's not to like in CxxTest? Not much, really. Other than wishing that the test syntax were a bit tighter, the only thing to watch out for is what happens with large projects. If you follow the examples in the documentation, it will create a single runner for all the tests you give it. This can be problematic if you're going to be having thousands of tests, and then making one small change in one of them causes a full recompilation of all your code.

Update: After talking with Erez and re-checking the documentation, I realized this is already fully supported in CxxUnit. By default, when you generate a test runner, it adds a main function and some global variables, so linking with other similar runners gives all sorts of problems. However, it turns out you can generate a test runner with the --part argument, and it will leave out the main function and any other globals. You can then link together all the runners and have a single executable. I wonder if it would be worth going as far as creating a runner for every suite, or if it would be best to cluster suites together. Worth investigating at some point whenever I get enough tests to make a difference.

Conclusion

After going through all six C++ unit-testing frameworks, four stand out as reasonable candidates: CppUnit, Boost.Test, a modified CppUnitLite, and CxxTest.

Of the four, CxxTest is my new personal favorite. It fits very closely the requirements of my ideal framework by leveraging the power of an external scripting language. It's very usable straight out of the "box" and it provides some nifty advanced features and great assert functionality. It does require the use of a scripting language as part of the build process, so those unconfortable with that requirement, might want to look at one of the other three frameworks.

CppUnit is a solid, complete framework. It has come a long, long way in the last few years. The major drawbacks are the relative verbosity for adding new tests and fixtures, as well as the reliance on STL and some advanced language issues.

If what you need is absolute simplicity, you can do no wrong starting with CppUnitLite (or a modified version), and tweaking it to fit your needs. It's a well-structured, ultra-light framework with no external dependencies, so modifying it is extremely easy. Its main drawback is the lack of features and the primitive assert functionality.

If you're going to be working mostly on the PC, you don't expect to have to modify the framework itself, and you don't mind pulling in some additional Boost libraries, Boost.Test could be an excellent choice.

Should you roll your own unit-test framework? I know that Kent Beck recommends it in his book Test-Driven Development: By Example , and it might be a great learning experience, but I just can't recommend it. Just as it's probably good to write a linked-list and a stack data structure a few times but I wouldn't recommend actually doing that in production code instead of using the ones provided in the STL, I strongly recommend starting with one of the three unit-testing frameworks mentioned above. If you really feel the need to roll your own, grab CppUnitLite and get hacking.

Whichever one you choose, you can really do no wrong with one of those three frameworks. The most important thing is that you are writing unit tests, or, even better, doing test-driven development. To paraphrase Michael Feathers, code without unit tests is legacy code, and you don't want to be writing legacy code, do you?

12月21日

CppUnit代码简介 - 第一部分,核心类

某种意义上说,CppUnit的代码并不是很好的C++代码。正因为它不是很好的C++代码,并且代码量不是很大(主库80K),所以我觉得比较适合想大量使用CppUnit并且需要深入了解的人或是初步涉足C++,想阅读一些简单的源代码/库的人。

这篇文章不适合于从未使用过CppUnit的人,如果你从未使用过CppUnit,但是对于测试驱动开发很感兴趣,可以参阅我的另一篇文章:CppUnit入门。

I. 目的

前一段时间拿到公司新的引擎,发现里面使用了CppUnit来做单元测试,于是小小的研究了一把,顺便把阅读CppUnit代码的心得写下,和大家分享。某种意义上说,CppUnit的代码并不是很好的C++代码,这主要是由历史原因引起的:首先CppUnit是对JUnit的一个移植,所以很多地方是把C++作为Java在用,从风格上到语义上,都是Java的;当然这也无可厚非,因为对于移植来说,只要能够做到和原先一样用起来,别的都是第二位的。其次,CppUnit(过度的?)考虑了兼容性,为了在不同的C++编译器和标准库下都能使用,它给自己制定了很多编码标准,譬如说"不要使用mutable"、"不要使用typename"、甚至"不要使用STL容器中的at成员",...。正因为它不是很好的C++代码,并且代码量不是很大(主库80K),所以我觉得比较适合想大量使用CppUnit并且需要深入了解的人或是初步涉足C++,想阅读一些简单的源代码/库的人(如果你很有经验,想学习最新的C++技术,或是想阅读大型的库,那么你可以阅读别的开源库。)

这篇文章不适合于从未使用过CppUnit的人,如果你从未使用过CppUnit,但是对于测试驱动开发很感兴趣,可以参阅我的另一篇文章:CppUnit入门。

II. 代码

CppUnit作为一个UnitTest框架,它的主要机制就是:让你为每一段代码写出测试用例,并且把大量的测试用例按照某种方式管理起来,并且提供给你不同的界面以及输出方式。所以我觉得它的代码也可以分为相应的部分:
1.测试用例及其管理,这部分只是管理和测试相关的数据结构,它们往往是静态的初始化的,在真正的测试还没有进行前就已经完全执行完毕了。
2.实际进行测试的代码,这部分包含了测试的初始化,测试中的异常处理以及测试进程和结果的通知等。这部分代码是你的测试运行中所执行的代码。
3.测试的界面和输入。譬如说,CppUnit提供了一个基于MFC的测试界面,你可以选择一些测试用例进行测试,并且获得结果。这部分代码我们将不作详细介绍,因为和测试本身其实关系不大。

III. 测试用例及其管理

这部分的类有Test、TestLeaf、TestCase、TestComposite和TestSuite以及一些像TestPath之类的辅助类。这一部分代码其实和UnitTest本身毫无关系,它只是一个对象树的管理。CppUnit把测试系统中所有可以用来测试的对象作为一棵树进行管理,每一个测试都是树上的一个节点,这些节点都是以Test为父类的。

1. TestPath

如果把整个测试用例树比作为文件树的话,TestPath表示的是一个相对/绝对路径。它内部保存的是一个Test的队列,也就是说,它把一个类似于/root/suite/case或者是/suite/child/case的路径变为一个路径上所经过的Test对象的队列。通过这个结构,我可以很方便的在树结构中的节点间随意移动。

2. Test

Test类中,除了虚析构函数和virtual void run( TestResult *result )=0 这个纯虚函数用来执行测试以外,别的函数都是用来做树管理的。因为树是递归定义的,作为一个节点,它只需要能够枚举自身的子节点就可以了。这些相关的函数为:countTestCases用来返回这个节点及其子节点中包含了多少个有效的Test对象,这样做主要是处于计数的目的,因为Test树上很多节点本身并不包含任何测试代码;getChildTestCount返回直接的子节点个数;getChildTestAt接受一个索引,返回相应的子节点;getName返回这个节点的名字。上述函数都是纯虚函数,由子类给出具体定义。
getChildTestAt是一个虚函数,它内部实际调用的是checkIsValidIndex和doGetChildTestAt,并且CppUnit的设计者也建议你不要重载它,而是重载doGetChildTestAt函数。这个是GoF里面的Template Method模式,不过既然这里不建议重载,就不应该使用virtual函数。或者和可能是JUnit的影子。
Test类真正实现的功能是对非直接子节点的处理。如前所述,对于直接子节点可以通过一个index来表示,而对于非直接子节点则是通过TestPath来表示的。Test提供了findTestPath、findTest以及resolveTestPath函数来对这些功能提供支持。虽然这些函数是以虚函数的形式提出的,也就是说理论上可以进行扩展,但是事实上在整个CppUnit体系中,只有Test类对它们作了实现。

3. TestLeaf

TestLeaf是Test的子类,它代表了Test树上的一个叶节点。它的实现很简单,对于countTestCases返回1,因为它自身就是一个有效的Test;getChildTestCount则返回0,因为没有子节点;它的doGetChildTestAt在正常情况下根本不应该被调用。

4. TestComposite

TestComposite也是Test的子类,它代表了Test树上的一个非根节点,这里对它的使用类似于一个GoF中Composite模式。它的countTestCases返回的是对所有子节点的countTestCases的累加,从这点上可以看出,TestComposite本身只是一个Test的容器,不作为一个有效的Test。TestComposite的run函数会调用自身的doStartSuite函数,在这个函数中,会对TestResult的startSuite进行调用;然后run函数再通过doRunChildTests函数间接调用所有子节点的run,最后通过doEndSuite函数间接调用TestResult的endSuite函数。TestComposite的run函数其实也是一个Template Method模式,因为通常希望通过这三个do****函数来对其进行定制。对于TestRunner::startSuite/endSuite的调用是为了让传入的TestRunner在每个Suite被测试的前后都得到通知,以做一些簿记工作。(当然窃以为这里的名字最好叫startComposite/endComposite。)

5. TestCase

TestCase继承自TestLeaf和TestFixture,是整个CppUnit中最常用最重要的类之一。顾名思义,TestCase就代表了通常UnitTest中所指的测试用例,也是整个Test树中最常用的叶节点。这个类既在Test树中有它的位置,也直接的参与测试的进行,所以被放在下一节介绍。

6. TestSuite

TestSuite是从TestComposite继承而来的,TestComposite为那些非根节点提供了运行机制,而TestSuite则在此基础上提供了对于子节点的管理。譬如说,通过addTest函数可以加入Test,deleteContents删除所有的子Test对象,它还实现了getChildTestCount和doGetChildTestAt函数以返回子节点的个数和指针。

IV. 实际进行测试的代码

这些类包括TestFixture、TestCase、TestCaller、TestRunner、TestListener、TestResult和Protector类体系。

1. TestFixture
与其说Test类是所有Test的根,不如说TestFixture是CppUnit中所有"测试"的根。因为仅仅从Test类继承而来的类只是这个体系的"管理部门",而从TestFixture继承而来的类,才是真正进行测试的"执行部门"。TestFixture除了一个虚析构函数(C++中"请从我派生"的代名词)以外,就定义了两个虚函数:setUp和tearDown,前者初始化一次测试,后者清除一次测试所产生的所有副作用。这三个函数在TestFixture中的定义都是空的。

2. TestCase
这是我们第二次看到它了,因为它处于两个类体系的交汇点,使用多重继承从TestLeaf和TestFixture继承。这个类既没有对TestLeaf中对于作为Test树子节点方面的功能进行加强,也没有重新定义TestFixture中为空的setUp和tearDown。唯一做的就是定义了run。它的run里面,会先调用TestRunner::startCase,然后调用setUp,接着调用TestCase中新定义的runTest函数,接着调用tearDown和TestRunner::endCase。从注释中可以看出作者希望把runTest作为一个纯虚函数定义,也就是说,其实你可以从TestCase派生,并且定义一个自己的runTest函数,以进行一些简单/单一的测试工作。如果要进行复杂的测试工作/构建复杂的测试用例树,那么应该使用别的机制。这样说的原因有两方面:首先从管理角度说,如果你有几项测试任务,就应该把它们作为Test树中的不同节点,这样你可以对总任务数/失败数进行统计,而TestCase是一个TestLeaf,如果你把它们堆砌在一个TestCase中,不利于管理;其次,如果你有几项测试任务要做,并且共享同样的初始化/清除代码,那么你想从TestCase派生来做这件事情,就必须重写setUp、tearDown和run。重写前两者也算了,要是连run也重写了,这,我干嘛还从TestCase派生?

3. TestCaller
TestCaller是一个GoF中的Adapter模式,它可以把任意一个定义了setUp/tearDown的类的对象包装为一个TestCase,并且在runTest中对这个对象的某个函数进行调用。也就是说,如果我有一个TestFixture的派生类FooBarTest,其中有一个fooTest和一个barTest函数,那么TestCaller<FooBarTest>(FooBarTest::fooTest)以及TestCaller<FooBarTest>(FooBarTest::batTest)就是两个TestCase派生类,它们的实例可以作为TestCase被加入到Test树中,也可以独立的进行测试运行。
TestCaller从TestCase派生而来,并且接受某个类的实例(也可以自己通过new生成一个)以及那个方法的指针作为构造函数参数并且保存在内部,在setUp和tearDown函数中,它调用了那个对象的setUp和tearDown,并且在runTest函数中使用保存的对象对它的指定的成员函数进行调用。使用TestCaller的好处是,你可以把一组相关的Test任务放在某个从TestFixture派生而来的类中,并且用TestCaller把它们包装成若干个TestCase。这样一来便于对相关的测试任务的管理,二来也能让不同的任务成为Test树的不同子节点。

4. TestListener
TestListener其实是一个测试事件的接收器,它定义了startTest、endTest、startSuite、endSuite、startTestRun、endTestRun和addFailure方法,这些都是空的虚函数,你可以定义自己的派生类并且对自己感兴趣的事件进行处理。

5. TestResult
TestResult是从SynchronizedObject继承而来的,SynchronizedObject其实就是对于Java中synchronized的模拟,SynchronizedObject和SynchronizedObject::ExclusiveZone是一个典型的用RAII来对某一个作用域进行互斥访问的例子。TestResult定义了addListener和removeListener方法来管理事件的订阅者,并且它也定义了所有Listener的方法,当你对TestResult的某个事件方法调用时,它会把这个事件发送给所有的Listener,不过CppUnit的开发者并不认为TestResult是一个TestListener,所以即使它同相同的方法实现了TestListener的所有函数,也没有从TestListener继承。
TestResult内部维护了一个测试过程是否被强行中止的标志,并且通过reset、stop和shouldStop对其进行管理,这给予运行中的测试一个响应强行中止的机会。
TestResult增加了几个函数,runTest就是其中之一,它接收一个Test的指针作为参数,并且调用这个Test对象的run。当你有一个Test对象和一个TestResult对象的时候,你可以通过Test::run(TestResult*)或者TestResult::runTest(Test*)来完成一次测试任务,区别在于后者在调用Test::run的前后会对TestResult::startTestRun和TestResult::endTestRun进行调用。
另一些比较有趣的函数是protect、pushProtector和popProtector。这三个函数其实是维护了一个ProtectorChain对象,在protect中调用了ProtectorChain::protect来为测试提供一个受保护的环境。

6.  Protector类体系
大家都知道,我们运行某个测试任务时,可以根据返回值来判断成功还是失败,可是有些"不良"函数会抛出异常,我们必须对异常进行捕捉,否则就不能进入下一个测试而会提前用一种极其可悲的方式结束。基本的Protector只提供了一些报错的辅助函数。通过查看DefaultProtector::protect的代码,可以得知在它的protect中,会尝试捕获Exception和std::exception,并且对所有未知的异常用...进行捕捉。DefaultProtector适合作为一个"最后的选择"来使用,因为通常我们希望知道我们是否抛出了某个或者多个特定的异常,这需要使用ProtectorChain。ProtectorChain::protect中有这样一段代码:

Functors functors;
for ( int index = m_protectors.size()-1; index >= 0; --index )
{
 const Functor &protectedFunctor = functors.empty() ? functor : *functors.back ();
 functors.push_back( new ProtectFunctor( m_protectors[index], protectedFunctor, context ) );
}

这段代码中,m_protectors是一组Protector,这段代码通过ProtectFunctor来把这些Protector连接在一起,并且它的根是functor,也就是那个需要保护的函数。最后对这个functors最后一个元素调用的时候,它先建立自己的保护机制,然后调用它的functor,那个functor就是它的前一个元素,可能是另一个Protector,也可能是最原始的受保护protector,这样一来,也就是说,所有的保护被一层一层的嵌套起来。

V. 小结

其实整个CppUnit中还有不少别的有意义的代码,譬如说它的测试结果输出机制,它的那些辅助宏和TestSuite的全局注册机制等,我将在以后的文章中介绍。

CppUnit - 测试驱动开发入门

测试驱动开发是一个现在软件界最流行的词汇之一,可是很多人还是不得其门而入。这篇文章想通过对于CppUnit的介绍,给予读者一个基本的映像。如果你熟知CppUnit的使用,请参阅我的另一篇文章:CppUnit代码简介 - 第一部分,核心类来获得对于CppUnit进一步的了解。

I. 前言
测试驱动开发是一个现在软件界最流行的词汇之一,可是很多人还是不得其门而入。这篇文章想通过对于CppUnit的介绍,给予读者一个基本的映像。如果你熟知CppUnit的使用,请参阅我的另一篇文章:CppUnit代码简介 - 第一部分,核心类来获得对于CppUnit进一步的了解。

II. 测试驱动开发
要理解测试驱动开发,必须先理解测试。测试就是通过对源代码的运行或者别的方式的检测来确定源代码之中是否含有已知或者未知的错误。所谓测试驱动开发,就是在开发前根据对将要开发的程序的要求,先写好所有测试代码,并且在开发过程中不时地通过运行测试代码来获得所开发的代码与所要求的结果之间的差距。很多人可能会有疑问:既然我还没有开始写代码,我怎么能够写测试代码呢?这是因为,虽然我们还没有写出任何实现代码,但是我们可以根据我们对代码的要求从使用者的角度写出测试代码。事实上,在开发前写出测试代码,可以检测你的要求是不是完善和精确,因为如果你写不出测试代码,表示你的需求还不够清晰。
这篇文章通过一个文件状态操作类来展示测试驱动开发相对于普通开发方法的优势。

III. 文件状态操作类(FileStatus)需求
构造函数,接受一个const std::string&作为文件名参数。
DWORD getFileSize()函数,获取这个文件的长度。
bool fileExists()函数,获取这个文件是否存在。
void setFileModifyDate(FILETIME ft)函数,设定这个文件的修改日期。
FILETIME getFileModifyDate()函数,返回这个文件的修改日期。
std::string getFileName()函数,返回这个文件的名字。

IV. CppUnit简介
我们所进行的测试,某种意义上说,就是一个或者多个函数。通过对这些函数的运行,我们可以检测我们是否有错误。假设我们要对构造函数和getFileName函数进行测试,这里面有一个很显然的不变式,就是对一个FileStatus::getFileName函数的调用,应该与传给这个FileStatus对象的构造函数的参数相同。于是我们有这样一个函数:
bool testCtorAndGetFileName()
{
    const string fileName( "a.dat" );
    FileStatus status( fileName );
    return ( status.getFileName() == fileName );
}
我们只需要测试这个函数的返回值就可以知道是否正确了。在CppUnit中,我们可以从TestCase派生出一个类,并且重载它的runTest函数。

class MyTestCase:public CPPUNIT_NS::TestCase
{
public:
    virtual void runTest()
    {
        const std::string fileName( "a.dat" );
        FileStatus status( fileName );
        CPPUNIT_ASSERT_EQUAL( status.getFileName(), fileName );
    }
};

CPPUNIT_ASSERT_EQUAL是一个宏,在它的两个参数不相等的时候,会抛出异常。所以,理论上说,我们可以通过:

MyTestCase m;
m.runTest();

来进行测试,如果有异常抛出,那么就说明代码写错了。可是,这显然不方便,也不是我们使用CppUnit的初衷。下面我们给出完整的代码:

// UnitTest.cpp : Defines the entry point for the console application.
//

#include "CppUnit/TestCase.h"
#include "CppUnit/TestResult.h"
#include "CppUnit/TextOutputter.h"
#include "CppUnit/TestResultCollector.h"

#include <string>
#include <iostream>

class FileStatus
{
    std::string mFileName;
public:
    FileStatus( const std::string& fileName ):mFileName( fileName )
    {}
    std::string getFileName() const
    {
        return mFileName;
    }
};

class MyTestCase:public CPPUNIT_NS::TestCase
{
public:
    virtual void runTest()
    {
        const std::string fileName( "a.dat" );
        FileStatus status( fileName );
        CPPUNIT_ASSERT_EQUAL( status.getFileName(), fileName );
    }
};

int main()
{
    MyTestCase m;
    CPPUNIT_NS::TestResult r;
    CPPUNIT_NS::TestResultCollector result;
    r.addListener( &result );
    m.run( &r );
    CPPUNIT_NS::TextOutputter out( &result, std::cout );
    out.write();
    return 0;
}

这里我先说一下怎样运行这个程序。假设你的CppUnit版本是1.10.2,解压后,你会在src文件夹中,发现一个CppUnitLibraries.dsw,打开它,并且编译。你会在lib文件夹中,发现一些lib和dll,我们的程序需要依赖当中的某些。接着,创建一个Console应用程序,假设我们仅使用Debug模式,在Project Settings中,把预编译选项(Precompiled Header)选成No,把CppUnit的include路径加入到Additional Include Directories中,并且把Code Generation改成Multi-threaded Debug Dll, 接着把CppUnitD.lib加入到你的项目中去。最后把我们的这个文件替换main.cpp。这个时候,就可以编译运行了。
这个文件中,前面四行分别是CppUnit相应的头文件,在CppUnit中,通常某个类就定义在用它的类名命名的头文件中。接着是我们的string和iostream头文件。然后是我们类的一个简单实现,只实现了这个测试中有意义的功能。接下去是我们的TestCase的定义,CPPUNIT_NS是CppUnit所在的名字空间。main中,TestResult其实是一个测试的控制器,你在调用TestCase的run时,需要提供一个TestResult。run作为测试的进行方,会把测试中产生的信息发送给TestResult,而TestResult作为一个分发器,会把所收到的信息再转发给它的Listener。也就是说,我简单的定义一个TestResult并且把它的指针传给TestCase::run,这个程序也能够编译通过并且正确运行,但是它不会有任何输出。TestResultCollector可以把测试输出的信息都收集起来,并且最后通过TextOutputter输出出来。在上述的例子中,你所获得的输出是:

OK (1 tests)

这说明我们一共进行了1个测试,并且都通过了。如果我们人为地把"return mFileName;"改成"return mFileName + 'a';"以制造一个错误,那么测试的结果就会变成:

!!!FAILURES!!!
Test Results:
Run:  1   Failures: 1   Errors: 0


1) test:  (F) line: 31 c:unittestunittest.cpp
equality assertion failed
- Expected: a.data
- Actual  : a.dat

这个结果告诉我们我们的实现出现了问题。前面说到,CPPUNIT_ASSERT_EQUAL在两个参数不等时会抛出异常,可是这里为什么没有异常退出了?这是因为,我们执行每一个TestCase的run的时候,它使用了一种特殊的机制把函数包起来,任何异常都会被捕获。具体细节请参考我的CppUnit代码简介一文。

如果我们把#include "CppUnit/TextOutputter.h"替换成#include "CppUnit/CompilerOutputter.h",并且把TextOutputter替换成CompilerOutputter,输出就变成:

c:unittestunittest.cpp(32) : error : Assertion
Test name:
equality assertion failed
- Expected: a.data
- Actual  : a.dat

Failures !!!
Run: 1   Failure total: 1   Failures: 1   Errors: 0

这个输出,在编译器的信息窗口里面,可以通过双击文件名加行号的那一行来到达相应的位置。

V. 迭代开发

上面的例子中我们先针对需求的一部分写了测试用例,然后就实现了相应的功能。我们可以在这些功能被测试后,继续实现别的功能的测试用例,然后继续实现相应的功能,这是一个迭代的过程,我们不断地增加测试用例和实现代码,最后达成需求。还有一种方法是,先写好所有的测试用例(这个时候通常会编译不通过),然后再添加能够让编译通过所需要的实现(这个时候通常运行测试会有很多错误),接着通过正确实现使得没有任何测试错误,最后,对代码作优化和更新,并且不断的保证测试通过。在这里我们着重介绍第二种方法。首先我们先写下所有的测试用例,在这里,由于有很多测试用例,我们不再使用TestCase,因为TestCase通常用在单一测试任务的情况下。这次我们从TestFixture派生我们的测试类:

class MyTestCase:public CPPUNIT_NS::TestFixture
{
public:
    void testCtorAndGetName()
    {
        const std::string fileName( "a.dat" );
        FileStatus status( fileName );
        CPPUNIT_ASSERT_EQUAL( status.getFileName(), fileName );
    }
    void testGetFileSize()
    {
        const std::string fileName( "a.dat" );
        FileStatus status( fileName );
        CPPUNIT_ASSERT_EQUAL( status.getFileSize(), 0 );//?
    }
};

写到这里,我们发现了两个问题,首先我们不停的初始化一些测试所需的对象,重复了很多代码;其次我们发现了一个接口设计错误,我们的接口设计上没有考虑一个文件不存在的情况。从中可见,先写好测试用例,不仅是对实现的测试,也是对我们设计的测试。TestFixture定义了两个成员函数setUp和tearDown,在每一个测试用例被执行的时候,和它定义在同一个类内部的setUp和tearDown会被调用以进行初始化和清除工作。我们可以用这两个函数来进行统一的初始化代码。并且,我们修改getFileSize、setFileModifyDate和getFileModifyDate使得它们在出现错误的时候,抛出异常FileStatusError。下面是我们的测试用例:

class MyTestCase:public CPPUNIT_NS::TestFixture
{
    std::string mFileNameExist;
    std::string mFileNameNotExist;
    std::string mTestFolder;
    enum DUMMY
    {
        FILE_SIZE = 1011
    };
public:
    virtual void setUp()
    {
        mTestFolder = "c:justfortest";
        mFileNameExist = mTestFolder + "exist.dat";
        mFileNameNotExist = mTestFolder + " notexist.dat";
        if( GetFileAttributes( mTestFolder.c_str() ) != INVALID_FILE_ATTRIBUTES )
            throw std::exception( "test folder already exists" );
        if( ! CreateDirectory( mTestFolder.c_str () ,NULL ) )
            throw std::exception( "cannot create folder" );
        HANDLE file = CreateFile( mFileNameExist.c_str(), GENERIC_READ | GENERIC_WRITE,
            0, NULL, CREATE_NEW, 0, NULL );
        if( file == INVALID_HANDLE_VALUE )
            throw std::exception( "cannot create file" );
        char buffer[FILE_SIZE];
        DWORD bytesWritten;
        if( !WriteFile( file, buffer, FILE_SIZE, &bytesWritten, NULL ) ||
            bytesWritten != FILE_SIZE )
        {
            CloseHandle( file );
            throw std::exception( "cannot write file" );
        }
        CloseHandle( file );
    }
    virtual void tearDown()
    {
        if( ! DeleteFile( mFileNameExist.c_str() ) )
            throw std::exception( "cannot delete file" );
        if( ! RemoveDirectory( mTestFolder.c_str() ) )
            throw std::exception( "cannot remove folder" );
    }
    void testCtorAndGetName()
    {
        FileStatus status( mFileNameExist );
        CPPUNIT_ASSERT_EQUAL( status.getFileName(), mFileNameExist );
    }
    void testGetFileSize()
    {
        FileStatus exist( mFileNameExist );
        //这里FILE_SIZE缺省是int,而getFileSize返回DWORD,不加转换会导致模版不能正确匹配。
        CPPUNIT_ASSERT_EQUAL( exist.getFileSize(), (DWORD)FILE_SIZE );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT_THROW( notExist.getFileSize(), FileStatusError );
    }
    void testFileExist()
    {
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT( exist.fileExist() );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT( ! notExist.fileExist() );
    }
    void testFileModifyDateBasic()
    {
        FILETIME fileTime;
        GetSystemTimeAsFileTime( &fileTime );
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_NO_THROW( exist.getFileModifyDate() );
        CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT_THROW( notExist.getFileModifyDate(), FileStatusError );
        CPPUNIT_ASSERT_THROW( notExist.setFileModifyDate( &fileTime ), FileStatusError );
    }
    void testFileModifyDateEqual()
    {
        FILETIME fileTime;
        GetSystemTimeAsFileTime( &fileTime );
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
        FILETIME get = exist.getFileModifyDate();
        // 这里 FILETIME 没有定义 operator==,所以不能直接使用 CPPUNIT_ASSERT_EQUAL
        CPPUNIT_ASSERT( CompareFileTime( &get, &fileTime ) == 0 );
    }
};

接着我们编写一个FileStatus类的骨架,使得这段测试代码可以被编译通过。

class FileStatusError
{};

class FileStatus
{
public:
    FileStatus(const std::string& fileName)
    {}
    DWORD getFileSize() const
    {
        return 0;
    }
    bool fileExist() const
    {
        return false;
    }
    void setFileModifyDate( const FILETIME* )
    {
    }
    FILETIME getFileModifyDate() const
    {
        return FILETIME();
    }
    std::string getFileName() const
    {
        return "";
    }
};

下面给出完整的程序:

// UnitTest.cpp : Defines the entry point for the console application.
//

#include "CppUnit/TestCase.h"
#include "CppUnit/TestResult.h"
#include "CppUnit/TextOutputter.h"
#include "CppUnit/TestResultCollector.h"
#include "CppUnit/TestCaller.h"
#include "CppUnit/extensions/HelperMacros.h"

#include <string>
#include <iostream>
#include <exception>
#include <windows.h>

class FileStatusError
{};

class FileStatus
{
public:
    FileStatus(const std::string& fileName)
    {}
    DWORD getFileSize() const
    {
        return 0;
    }
    bool fileExist() const
    {
        return false;
    }
    void setFileModifyDate( const FILETIME* )
    {
    }
    FILETIME getFileModifyDate() const
    {
        return FILETIME();
    }
    std::string getFileName() const
    {
        return "";
    }
};

class MyTestCase:public CPPUNIT_NS::TestFixture
{
    std::string mFileNameExist;
    std::string mFileNameNotExist;
    std::string mTestFolder;
    enum DUMMY
    {
        FILE_SIZE = 1011
    };
public:
    virtual void setUp()
    {
        mTestFolder = "c:justfortest";
        mFileNameExist = mTestFolder + "exist.dat";
        mFileNameNotExist = mTestFolder + " notexist.dat";
        if( GetFileAttributes( mTestFolder.c_str() ) != INVALID_FILE_ATTRIBUTES )
            throw std::exception( "test folder already exists" );
        if( ! CreateDirectory( mTestFolder.c_str () ,NULL ) )
            throw std::exception( "cannot create folder" );
        HANDLE file = CreateFile( mFileNameExist.c_str(), GENERIC_READ | GENERIC_WRITE,
            0, NULL, CREATE_NEW, 0, NULL );
        if( file == INVALID_HANDLE_VALUE )
            throw std::exception( "cannot create file" );
        char buffer[FILE_SIZE];
        DWORD bytesWritten;
        if( !WriteFile( file, buffer, FILE_SIZE, &bytesWritten, NULL ) ||
            bytesWritten != FILE_SIZE )
        {
            CloseHandle( file );
            throw std::exception( "cannot write file" );
        }
        CloseHandle( file );
    }
    virtual void tearDown()
    {
        if( ! DeleteFile( mFileNameExist.c_str() ) )
            throw std::exception( "cannot delete file" );
        if( ! RemoveDirectory( mTestFolder.c_str() ) )
            throw std::exception( "cannot remove folder" );
    }
    void testCtorAndGetName()
    {
        FileStatus status( mFileNameExist );
        CPPUNIT_ASSERT_EQUAL( status.getFileName(), mFileNameExist );
    }
    void testGetFileSize()
    {
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_EQUAL( exist.getFileSize(), (DWORD)FILE_SIZE );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT_THROW( notExist.getFileSize (), FileStatusError );
    }
    void testFileExist()
    {
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT( exist.fileExist() );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT( ! notExist.fileExist() );
    }
    void testFileModifyDateBasic()
    {
        FILETIME fileTime;
        GetSystemTimeAsFileTime( &fileTime );
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_NO_THROW( exist.getFileModifyDate() );
        CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT_THROW( notExist.getFileModifyDate (), FileStatusError );
        CPPUNIT_ASSERT_THROW( notExist.setFileModifyDate( &fileTime ), FileStatusError );
    }
    void testFileModifyDateEqual()
    {
        FILETIME fileTime;
        GetSystemTimeAsFileTime( &fileTime );
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
        FILETIME get = exist.getFileModifyDate();
        CPPUNIT_ASSERT( CompareFileTime( &get, &fileTime ) == 0 );
    }
};

int main()
{
    CPPUNIT_NS::TestResult r;
    CPPUNIT_NS::TestResultCollector result;
    r.addListener( &result );
    CPPUNIT_NS::TestCaller<MyTestCase> testCase1( "testCtorAndGetName", MyTestCase::testCtorAndGetName );
    CPPUNIT_NS::TestCaller<MyTestCase> testCase2( "testGetFileSize", MyTestCase::testGetFileSize );
    CPPUNIT_NS::TestCaller<MyTestCase> testCase3( "testFileExist", MyTestCase::testFileExist );
    CPPUNIT_NS::TestCaller<MyTestCase> testCase4( "testFileModifyDateBasic", MyTestCase::testFileModifyDateBasic );
    CPPUNIT_NS::TestCaller<MyTestCase> testCase5( "testFileModifyDateEqual", MyTestCase::testFileModifyDateEqual );
    testCase1.run( &r );
    testCase2.run( &r );
    testCase3.run( &r );
    testCase4.run( &r );
    testCase5.run( &r );
    CPPUNIT_NS::TextOutputter out( &result, std::cout );
    out.write();
    return 0;
}

这里的TestCaller可以把从TestFixture派生而来的类的成员函数转化为一个TestCase。这段代码可以编译通过,运行后一共进行了5个测试,完全失败。这是我们意料之中的结果,因此我们进一步实现我们的功能,完成后的代码为:

// UnitTest.cpp : Defines the entry point for the console application.
//

#include "CppUnit/TestCase.h"
#include "CppUnit/TestResult.h"
#include "CppUnit/TextOutputter.h"
#include "CppUnit/TestResultCollector.h"
#include "CppUnit/TestCaller.h"
#include "CppUnit/extensions/HelperMacros.h"

#include <string>
#include <iostream>
#include <exception>
#include <windows.h>

class FileStatusError
{};

class FileStatus
{
    std::string mFileName;
public:
    FileStatus(const std::string& fileName):mFileName( fileName )
    {}
    DWORD getFileSize() const
    {
        DWORD fileSize = INVALID_FILE_SIZE;
        HANDLE file = CreateFile( mFileName.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
            NULL, OPEN_EXISTING, 0, NULL );
        if( file != INVALID_HANDLE_VALUE )
        {
            fileSize = GetFileSize( file, NULL );
            CloseHandle( file );
        }
        if( fileSize == INVALID_FILE_SIZE )
            throw FileStatusError();
        return fileSize;
    }
    bool fileExist() const
    {
        return GetFileAttributes( mFileName.c_str() ) != INVALID_FILE_ATTRIBUTES;
    }
    void setFileModifyDate( const FILETIME* fileTime )
    {
        BOOL result = FALSE;
        HANDLE file = CreateFile( mFileName.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
            NULL, OPEN_EXISTING, 0, NULL );
        if( file != INVALID_HANDLE_VALUE )
        {
            result = SetFileTime( file, NULL, NULL, fileTime );
            int i = GetLastError();
            CloseHandle( file );
        }
        if( ! result )
            throw FileStatusError();
    }
    FILETIME getFileModifyDate() const
    {
        FILETIME time;
        BOOL result = FALSE;
        HANDLE file = CreateFile( mFileName.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
            NULL, OPEN_EXISTING, 0, NULL );
        if( file != INVALID_HANDLE_VALUE )
        {
            result = GetFileTime( file, NULL, NULL, &time );
            CloseHandle( file );
        }
        if( ! result )
            throw FileStatusError();
        return time;
    }
    std::string getFileName() const
    {
        return mFileName;
    }
};

class MyTestCase:public CPPUNIT_NS::TestFixture
{
    std::string mFileNameExist;
    std::string mFileNameNotExist;
    std::string mTestFolder;
    enum DUMMY
    {
        FILE_SIZE = 1011
    };
public:
    virtual void setUp()
    {
        mTestFolder = "c:justfortest";
        mFileNameExist = mTestFolder + "exist.dat";
        mFileNameNotExist = mTestFolder + " notexist.dat";
        if( GetFileAttributes( mTestFolder.c_str() ) != INVALID_FILE_ATTRIBUTES )
            throw std::exception( "test folder already exists" );
        if( ! CreateDirectory( mTestFolder.c_str () ,NULL ) )
            throw std::exception( "cannot create folder" );
        HANDLE file = CreateFile( mFileNameExist.c_str(), GENERIC_READ | GENERIC_WRITE,
            0, NULL, CREATE_NEW, 0, NULL );
        if( file == INVALID_HANDLE_VALUE )
            throw std::exception( "cannot create file" );
        char buffer[FILE_SIZE];
        DWORD bytesWritten;
        if( !WriteFile( file, buffer, FILE_SIZE, &bytesWritten, NULL ) ||
            bytesWritten != FILE_SIZE )
        {
            CloseHandle( file );
            throw std::exception( "cannot write file" );
        }
        CloseHandle( file );
    }
    virtual void tearDown()
    {
        if( ! DeleteFile( mFileNameExist.c_str() ) )
            throw std::exception( "cannot delete file" );
        if( ! RemoveDirectory( mTestFolder.c_str() ) )
            throw std::exception( "cannot remove folder" );
    }
    void testCtorAndGetName()
    {
        FileStatus status( mFileNameExist );
        CPPUNIT_ASSERT_EQUAL( status.getFileName(), mFileNameExist );
    }
    void testGetFileSize()
    {
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_EQUAL( exist.getFileSize(), (DWORD)FILE_SIZE );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT_THROW( notExist.getFileSize (), FileStatusError );
    }
    void testFileExist()
    {
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT( exist.fileExist() );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT( ! notExist.fileExist() );
    }
    void testFileModifyDateBasic()
    {
        FILETIME fileTime;
        GetSystemTimeAsFileTime( &fileTime );
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_NO_THROW( exist.getFileModifyDate() );
        CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT_THROW( exist.getFileModifyDate(), FileStatusError );
        CPPUNIT_ASSERT_THROW( exist.setFileModifyDate( &fileTime ), FileStatusError );
    }
    void testFileModifyDateEqual()
    {
        FILETIME fileTime;
        GetSystemTimeAsFileTime( &fileTime );
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
        FILETIME get = exist.getFileModifyDate();
        CPPUNIT_ASSERT( CompareFileTime( &get, &fileTime ) == 0 );
    }
};

int main()
{
    CPPUNIT_NS::TestResult r;
    CPPUNIT_NS::TestResultCollector result;
    r.addListener( &result );
    CPPUNIT_NS::TestCaller<MyTestCase> testCase1( "testCtorAndGetName", MyTestCase::testCtorAndGetName );
    CPPUNIT_NS::TestCaller<MyTestCase> testCase2( "testGetFileSize", MyTestCase::testGetFileSize );
    CPPUNIT_NS::TestCaller<MyTestCase> testCase3( "testFileExist", MyTestCase::testFileExist );
    CPPUNIT_NS::TestCaller<MyTestCase> testCase4( "testFileModifyDateBasic", MyTestCase::testFileModifyDateBasic );
    CPPUNIT_NS::TestCaller<MyTestCase> testCase5( "testFileModifyDateEqual", MyTestCase::testFileModifyDateEqual );
    testCase1.run( &r );
    testCase2.run( &r );
    testCase3.run( &r );
    testCase4.run( &r );
    testCase5.run( &r );
    CPPUNIT_NS::TextOutputter out( &result, std::cout );
    out.write();
    return 0;
}

运行测试,发现两个错误:

1) test: testFileModifyDateBasic (F) line: 140 c:unittestunittest.cpp
assertion failed
- Unexpected exception caught


2) test: testFileModifyDateEqual (F) line: 150 c:unittestunittest.cpp
assertion failed
- Unexpected exception caught

调试发现,原来我的setFileModifyDate中,文件的打开方式为GENERIC_READ,只有读权限,自然不能写。把这个替换为 GENERIC_READ | GENERIC_WRITE,再运行,一切OK!
其实上面的测试以及实现代码还有一些问题,譬如说,测试用例分得还不够细,有些测试可以继续细分为几个函数,这样一旦遇到测试错误,你可以很精确的知道错误的位置(因为抛出异常错误是不能知道行数的)。不过用来说明怎样进行测试驱动开发应该是足够了。

VI. 测试集

CPPUNIT_NS::TestCaller<MyTestCase> testCase1( "testCtorAndGetName", MyTestCase::testCtorAndGetName );
CPPUNIT_NS::TestCaller<MyTestCase> testCase2( "testGetFileSize", MyTestCase::testGetFileSize );
CPPUNIT_NS::TestCaller<MyTestCase> testCase3( "testFileExist", MyTestCase::testFileExist );
CPPUNIT_NS::TestCaller<MyTestCase> testCase4( "testFileModifyDateBasic", MyTestCase::testFileModifyDateBasic );
CPPUNIT_NS::TestCaller<MyTestCase> testCase5( "testFileModifyDateEqual", MyTestCase::testFileModifyDateEqual );

这段代码虽然还不够触目惊心,但是让程序员来做这个,的确是太浪费了。CppUnit为我们提供了一些机制来避免这样的浪费。我们可以修改我们的测试代码为:

class MyTestCase:public CPPUNIT_NS::TestFixture
{
    std::string mFileNameExist;
    std::string mFileNameNotExist;
    std::string mTestFolder;
    enum DUMMY
    {
        FILE_SIZE = 1011
    };
    CPPUNIT_TEST_SUITE( MyTestCase );
        CPPUNIT_TEST( testCtorAndGetName );
        CPPUNIT_TEST( testGetFileSize );
        CPPUNIT_TEST( testFileExist );
        CPPUNIT_TEST( testFileModifyDateBasic );
        CPPUNIT_TEST( testFileModifyDateEqual );
    CPPUNIT_TEST_SUITE_END();
public:
    virtual void setUp()
    {
        mTestFolder = "c:justfortest";
        mFileNameExist = mTestFolder + "exist.dat";
        mFileNameNotExist = mTestFolder + "notexist.dat";
        if( GetFileAttributes( mTestFolder.c_str() ) != INVALID_FILE_ATTRIBUTES )
            throw std::exception( "test folder already exists" );
        if( ! CreateDirectory( mTestFolder.c_str() ,NULL ) )
            throw std::exception( "cannot create folder" );
        HANDLE file = CreateFile( mFileNameExist.c_str(), GENERIC_READ | GENERIC_WRITE,
            0, NULL, CREATE_NEW, 0, NULL );
        if( file == INVALID_HANDLE_VALUE )
            throw std::exception( "cannot create file" );
        char buffer[FILE_SIZE];
        DWORD bytesWritten;
        if( !WriteFile( file, buffer, FILE_SIZE, &bytesWritten, NULL ) ||
            bytesWritten != FILE_SIZE )
        {
            CloseHandle( file );
            throw std::exception( "cannot write file" );
        }
        CloseHandle( file );
    }
    virtual void tearDown()
    {
        if( ! DeleteFile( mFileNameExist.c_str() ) )
            throw std::exception( "cannot delete file" );
        if( ! RemoveDirectory( mTestFolder.c_str() ) )
            throw std::exception( "cannot remove folder" );
    }
    void testCtorAndGetName()
    {
        FileStatus status( mFileNameExist );
        CPPUNIT_ASSERT_EQUAL( status.getFileName(), mFileNameExist );
    }
    void testGetFileSize()
    {
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_EQUAL( exist.getFileSize(), (DWORD)FILE_SIZE );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT_THROW( notExist.getFileSize(), FileStatusError );
    }
    void testFileExist()
    {
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT( exist.fileExist() );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT( ! notExist.fileExist() );
    }
    void testFileModifyDateBasic()
    {
        FILETIME fileTime;
        GetSystemTimeAsFileTime( &fileTime );
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_NO_THROW( exist.getFileModifyDate () );
        CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
        FileStatus notExist( mFileNameNotExist );
        CPPUNIT_ASSERT_THROW( notExist.getFileModifyDate(), FileStatusError );
        CPPUNIT_ASSERT_THROW( notExist.setFileModifyDate( &fileTime ), FileStatusError );
    }
    void testFileModifyDateEqual()
    {
        FILETIME fileTime;
        GetSystemTimeAsFileTime( &fileTime );
        FileStatus exist( mFileNameExist );
        CPPUNIT_ASSERT_NO_THROW( exist.setFileModifyDate( &fileTime ) );
        FILETIME get = exist.getFileModifyDate();
        CPPUNIT_ASSERT( CompareFileTime( &get, &fileTime ) == 0 );
    }
};

CPPUNIT_TEST_SUITE_REGISTRATION( MyTestCase );

int main()
{
    CPPUNIT_NS::TestResult r;
    CPPUNIT_NS::TestResultCollector result;
    r.addListener( &result );
    CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest()->run( &r );
    CPPUNIT_NS::TextOutputter out( &result, std::cout );
    out.write();
    return 0;
}

这里的

    CPPUNIT_TEST_SUITE( MyTestCase );
        CPPUNIT_TEST( testCtorAndGetName );
        CPPUNIT_TEST( testGetFileSize );
        CPPUNIT_TEST( testFileExist );
        CPPUNIT_TEST( testFileModifyDateBasic );
        CPPUNIT_TEST( testFileModifyDateEqual );
    CPPUNIT_TEST_SUITE_END();

最重要的内容其实是定义了一个函数suite,这个函数返回了一个包含了所有CPPUNIT_TEST定义的测试用例的一个测试集。CPPUNIT_TEST_SUITE_REGISTRATION通过静态注册把这个测试集注册到全局的测试树中,最后通过CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest()生成一个包含所有测试用例的测试并且运行。具体的内部运行机制请参考CppUnit代码简介。

VII. 小节

这篇文章简要的介绍了CppUnit和测试驱动开发的基本概念,虽然CppUnit还有很多别的功能,譬如说基于GUI的测试环境以及和编译器Post Build相连接的测试输出,以及对于测试系统的扩展等,但是基本上掌握了本文中的内容就可以进行测试驱动的开发了。

此外,测试驱动开发还可以检验需求的错误。其实我选用GetFileTime和SetFileTime作为例子是因为,有些系统上,SetFileTime所设置的时间是有一定的精度的,譬如说按秒,按天,...,因此你设置了一个时间后,可能get回来的时间和它不同。这其实是一个需求的错误。当然由于我的系统上没有这个问题,所以我也就不无病呻吟了。具体可以参考MSDN中对于这两个函数的介绍。

12月20日

关于C++中RTTI的编码实现

摘要:

  RTTI (Run-Time Type Identification)是面向对象程序设计中一种重要的技术。现行的C++标准对RTTI已经有了明确的支持。不过在某些情况下出于特殊的开发需要,我们需要自己编码来实现。本文介绍了一些关于RTTI的基础知识及其原理和实现。  

RTTI需求:

  和很多其他语言一样,C++是一种静态类型语言。其数据类型是在编译期就确定的,不能在运行时更改。然而由于面向对象程序设计中多态性的要求,C++中的指针或引用(Reference)本身的类型,可能与它实际代表(指向或引用)的类型并不一致。有时我们需要将一个多态指针转换为其实际指向对象的类型,就需要知道运行时的类型信息,这就产生了运行时类型识别的要求。

   C++对RTTI的支持

  C++提供了两个关键字typeid和dynamic_cast和一个type_info类来支持RTTI:

  dynamic_cast操作符:它允许在运行时刻进行类型转换,从而使程序能够在一个类层次结构安全地转换类型。dynamic_cast提供了两种转换方式,把基类指针转换成派生类指针,或者把指向基类的左值转换成派生类的引用。见下例讲述:

 
void company::payroll(employee *pe) {
//对指针转换失败,dynamic_cast返回NULL
if(programmer *pm=dynamic_cast(pe)){
pm->bonus();
}
}
void company::payroll(employee &re) {
try{
//对引用转换失败的话,则会以抛出异常来报告错误
programmer &rm=dynamic_cast(re);
pm->bonus();
}
catch(std::bad_cast){

}
}

  这里bonus是programmer的成员函数,基类employee不具备这个特性。所以我们必须使用安全的由基类到派生类类型转换,识别出programmer指针。

  typeid操作符:它指出指针或引用指向的对象的实际派生类型。

  例如:

employee* pe=new manager;
typeid(*pe)==typeid(manager) //true

  typeid可以用于作用于各种类型名,对象和内置基本数据类型的实例、指针或者引用,当作用于指针和引用将返回它实际指向对象的类型信息。typeid的返回是type_info类型。

  type_info类:这个类的确切定义是与编译器实现相关的,下面是《C++ Primer》中给出的定义(参考资料[2]中谈到编译器必须提供的最小信息量):

class type_info {
private:
type_info(const type_info&);
type_info& operator=( const type_info& );
public:
virtual ~type_info();
int operator==( const type_info& ) const;
int operator!=( const type_info& ) const;
const char* name() const;
};

实现目标:

  实现的方案

  方案一:利用多态来取得指针或应用的实际类型信息

  这是一个最简单的方法,也是作者目前所采用的办法。

  实现:

enum ClassType{
UObjectClass,
URectViewClass,
UDialogClass,
……
};
class UObject{
virtual char* GetClassName() const {
return "UObject";
};
virtual ClassType TypeOfClass(){
return UObjectClass;
};
};
class UDialog{
virtual char* GetClassName() const {
return "UDialog";
};
virtual ClassType TypeOfClass(){
return UDialogClass;
};
};

  示例:

UObject po=new UObject;
UObject pr=new URectView;
UObject pd=new UDialog;
cout << "po is a " << po->GetClassName() << endl;
cout << "pr is a " << pr->GetClassName() << endl;
cout << "pd is a " << pd->GetClassName() << endl;
cout<TypeOfClass()==UObjectClass< cout<TypeOfClass()==URectViewClass<
cout<TypeOfClass()==UDialogClass<
cout< TypeOfClass()==UObjectClass<
cout<TypeOfClass()==UDialogClass<< td>

  输出:

po is a UObjectClass
pr is a URectViewClass
pd is a UDialogClass
true
true
true
false
false

  这种实现方法也就是在基类中提供一个多态的方法,这个方法返回一个类型信息。这样我们能够知道一个指针所指向对象的具体类型,可以满足一些简单的要求。

  但是很显然,这样的方法只实现了typeid的部分功能,还存在很多缺点:

  1、 用户每增加一个类必须覆盖GetClassName和TypeOfClass两个方法,如果忘了,会导致程序错误。

  2、 这里的类名和类标识信息不足以实现dynamic_cast的功能,从这个意义上而言此方案根本不能称为RTTI。

  3、 用户必须手工维护每个类的类名与标识,这限制了以库的方式提供给用户的可能。

  4、 用户必须手工添加GetClassName和TypeOfClass两个方法,使用并不方便。

  其中上面的部分问题我们可以采用C/C++中的宏技巧(Macro Magic)来解决,这个可以在我们的最终解决方案的代码中看到。下面采用方案二中将予以解决上述问题。

  方案二:以一个类型表来存储类型信息

  这种方法考虑使用一个类结构,除了保留原有的整型类ID,类名字符串外,增加了一个指向基类TypeInfo成员的指针。

struct TypeInfo
{
char* className;
int type_id;
TypeInfo* pBaseClass;
operator== (const TypeInfo& info){
return this==&info;
}
operator!= (const TypeInfo& info){
return this!=&info;
}
};

  从这里可以看到,以这种方式实现的RTTI不支持多重继承。所幸多重继承在程序设计中并非必须,而且也不推荐。下面的代码中,我将为DP9900软件项目组中类层次结构中的几个类添加RTTI功能。DP9900项目中,绝大部分的类都以单继承方式从UObject这个根类直接或间接继承而来。这样我们就可以从UObject开始,加入我们RTTI支持所需要的数据和方法。

class UObject
{
public:
bool IsKindOf(TypeInfo& cls); //判别某个对象是否属于某一个类
public:
virtual int GetTypeID(){return rttiTypeInfo.type_id;}
virtual char* GetTypeName(){return rttiTypeInfo.className;}
virtual TypeInfo& GetTypeInfo(){return rttiTypeInfo;}
static TypeInfo& GetTypeInfoClass(){return rttiTypeInfo;}
private:
static TypeInfo rttiTypeInfo;
};
//依次为className、type_id、pBaseClass赋值
TypeInfo UObject::rttiTypeInfo={"UObject",0,NULL};

  考虑从UObject将这个TypeInfo类作为每一个新增类的静态成员,这样一个类的所有对象将共享TypeInfo的唯一实例。我们希望能够在程序运行之前就为type_id,className做好初始化,并让pBaseClass指向基类的这个TypeInfo。

  每个类的TypeInfo成员约定使用rttiTypeInfo的命名,为了避免命名冲突,我们将其作为private成员。有了基类的支持并不够,当用户需要RTTI支持,还需要自己来做一些事情:

  1、 派生类需要从UObject继承。

  2、 添加rttiTypeInfo变量。

  3、 在类外正确初始化rttiTypeInfo静态成员。

  4、 覆盖GetTypeID、GetTypeName、GetTypeInfo、GetTypeInfoClass四个成员函数。

  如下所示:

class UView:public UObject
{
public:
virtual int GetTypeID(){return rttiTypeInfo.type_id;}
virtual char* GetTypeName(){return rttiTypeInfo.className;}
virtual TypeInfo& GetTypeInfo(){return rttiTypeInfo;}
static TypeInfo& GetTypeInfoClass(){return rttiTypeInfo;}
private:
static TypeInfo rttiTypeInfo;
};

  有了前三步,这样我们就可以得到一个不算太复杂的链表――这是一棵类型信息构成的"树",与数据结构中的树的唯一差别就是其指针方向相反。

  这样,从任何一个UObject的子类,顺着pBaseClass往上找,总能遍历它的所有父类,最终到达UObject。

  在这个链表的基础上,要判别某个对象是否属于某一个类就很简单。下面给出UObject::IsKindOf()的实现。

bool UObject::IsKindOf(TypeInfo& cls)
{
TypeInfo* p=&(this->GetTypeInfo());
while(p!=NULL){
if(p->type_id==cls.type_id)
return true;
p=p->pBaseClass;
}
return false;
}

  有了IsKindOf的支持,dynamic_cast的功能也就可以用一个简单的safe_cast来实现:

template
inline T* safe_cast(UObject* ptr,TypeInfo& cls)
{
return (ptr->IsKindOf(cls)?(T*)ptr:NULL);
}

  至此,我们已经能够从功能上完成前面的目标了,不过用户要使用这个类库的RTTI功能还很麻烦,要敲入一大堆对他们毫无意义的函数代码,要在初始化 rttiTypeInfo静态成员时手工设置类ID与类名。其实这些麻烦完全不必交给我们的用户,适当采用一些宏技巧(Macro Magic),就可以让C++的预处理器来替我们写很多枯燥的代码。关于宏不是本文的重点,你可以从最终代码清单看到它们。下面再谈谈关于类ID的问题。

  类ID

  为了使不同类型的对象可区分,用一个给每个TypeInfo对象一个类ID来作为比较的依据是必要的。
其实对于我们这里的需求和实现方法而言,其实类ID并不是必须的。每一个支持RTTI的类都包含了一个静态TypeInfo对象,这个对象的地址就是在进程中全局唯一。但考虑到其他一些技术如:动态对象创建、对象序列化等,它们可能会要求RTTI给出一个静态不变的ID。在本文的实现中,对此作了有益的尝试。

  首先声明一个用来产生递增类ID的全局变量。再声明如下一个结构,没有数据成员,只有一个构造函数用于初始化TypeInfo的类ID:

extern int TypeInfoOrder=0;
struct InitTypeInfo
{
InitTypeInfo(TypeInfo* info)
{
info->type_id=TypeInfoOrder++;
}
};

  为UObject添加一个private的静态成员及其初始化:

class UObject
{
//……
private:
static InitTypeInfo initClassInfo;
};
InitTypeInfo UObject::initClassInfo(&(UObject::rttiTypeInfo));

  并且对每一个从UObject派生的子类也进行同样的添加。这样您将看到,在C++主函数执行前,启动代码将替我们调用每一个类的 initClassInfo成员的构造函数InitTypeInfo::InitTypeInfo(TypeInfo* info),而正是这个函数替我们产生并设置了类ID。InitTypeInfo的构造函数还可以替我们做其他一些有用的初始化工作,比如将所有的 TypeInfo信息登录到一个表格里,让我们可以很方便的遍历它。

  但实践与查阅资料让我们发现,由于C++中对静态成员初始化的顺序没有明确的规定,所以这样的方式产生出来的类ID并非完全静态,换一个编译器编译执行产生的结果可能完全不同。

  还有一个可以考虑的方案是采用某种无冲突HASH算法,将类名转换成为一个唯一整数。使用标准CRC32算法从类型名计算出一个整数作为类ID也许是个不错的想法[3]。

  程序清单

// URtti.h
#ifndef __URTTI_H__
#define __URTTI_H__

class UObject;

struct TypeInfo
{
char* className;
int type_id;
TypeInfo* pBaseClass;
operator== (const TypeInfo& info){
return this==&info;
}
operator!= (const TypeInfo& info){
return this!=&info;
}
};

inline std::ostream& operator<< (std::ostream& os,TypeInfo& info)
{
return (os<< "[" << &info << "]" << "\t"
<< info.type_id << ":"
<< info.className << ":"
<< info.pBaseClass << std::endl);
}

extern int TypeInfoOrder;

struct InitTypeInfo
{
InitTypeInfo(/*TypeInfo* base,*/TypeInfo* info)
{
info->type_id=TypeInfoOrder++;
}
};

#define TYPEINFO_OF_CLASS(class_name) (class_name::GetTypeInfoClass())
#define TYPEINFO_OF_OBJ(obj_name) (obj_name.GetTypeInfo())
#define TYPEINFO_OF_PTR(ptr_name) (ptr_name->GetTypeInfo())

#define DECLARE_TYPEINFO(class_name) \
public: \
virtual int GetTypeID(){return TYPEINFO_MEMBER(class_name).type_id;} \
virtual char* GetTypeName(){return TYPEINFO_MEMBER(class_name).className;} \
virtual TypeInfo& GetTypeInfo(){return TYPEINFO_MEMBER(class_name);} \
static TypeInfo& GetTypeInfoClass(){return TYPEINFO_MEMBER(class_name);} \
private: \
static TypeInfo TYPEINFO_MEMBER(class_name); \
static InitTypeInfo initClassInfo; \

#define IMPLEMENT_TYPEINFO(class_name,base_name) \
TypeInfo class_name::TYPEINFO_MEMBER(class_name)= \
{#class_name,0,&(base_name::GetTypeInfoClass())}; \
InitTypeInfo class_name::initClassInfo(&(class_name::TYPEINFO_MEMBER(class_name)));

#define DYNAMIC_CAST(object_ptr,class_name) \
safe_cast(object_ptr,TYPEINFO_OF_CLASS(class_name))

#define TYPEINFO_MEMBER(class_name) rttiTypeInfo

class UObject
{
public:
bool IsKindOf(TypeInfo& cls);
public:
virtual int GetTypeID(){return TYPEINFO_MEMBER(UObject).type_id;}
virtual char* GetTypeName(){return TYPEINFO_MEMBER(UObject).className;}
virtual TypeInfo& GetTypeInfo(){return TYPEINFO_MEMBER(UObject);}
static TypeInfo& GetTypeInfoClass(){return TYPEINFO_MEMBER(UObject);}
private:
static TypeInfo TYPEINFO_MEMBER(UObject);
static InitTypeInfo initClassInfo;
};

template
inline T* safe_cast(UObject* ptr,TypeInfo& cls)
{
return (ptr->IsKindOf(cls)?(T*)ptr:NULL);
}
#endif
// URtti.cpp
#include "urtti.h"

extern int TypeInfoOrder=0;

TypeInfo UObject::TYPEINFO_MEMBER(UObject)={"UObject",0,NULL};
InitTypeInfo UObject::initClassInfo(&(UObject::TYPEINFO_MEMBER(UObject)));

bool UObject::IsKindOf(TypeInfo& cls)
{
TypeInfo* p=&(this->GetTypeInfo());
while(p!=NULL){
if(p->type_id== cls.type_id)
return true;
p=p->pBaseClass;
}
return false;
}
// mail.cpp
#include
#include "urtti.h"
using namespace std;

class UView:public UObject
{
DECLARE_TYPEINFO(UView)
};
IMPLEMENT_TYPEINFO(UView,UObject)

class UGraph:public UObject
{
DECLARE_TYPEINFO(UGraph)
};
IMPLEMENT_TYPEINFO(UGraph,UObject)

void main()
{
UObject* po=new UObject;
UView* pv=new UView;
UObject* pg=new UGraph;
if(DYNAMIC_CAST(po,UView))
cout << "po => UView succeed" << std::endl;
else
cout << "po => UView failed" << std::endl;
if(DYNAMIC_CAST(pv,UView))
cout << "pv => UView succeed" << std::endl;
else
cout << "pv => UView failed" << std::endl;
if(DYNAMIC_CAST(po,UGraph))
cout << "po => UGraph succeed" << std::endl;
else
cout << "po => UGraph failed" << std::endl;
if(DYNAMIC_CAST(pg,UGraph))
cout << "pg => UGraph succeed" << std::endl;
else
cout << "pg => UGraph failed" << std::endl;
}

  实现结果

  本文实现了如下几个宏来支持RTTI,它们的使用方法都可以在上面的代码中找到:
  
宏函数 功能及参数说明
DECLARE_TYPEINFO(class_name) 为类添加RTTI功能放在类声明的起始位置
IMPLEMENT_TYPEINFO(class_name,base) 同上,放在类定义任何位置
TYPEINFO_OF_CLASS(class_name) 相当于typeid(类名)
TYPEINFO_OF_OBJ(obj_name) 相当于typeid(对象)
TYPEINFO_OF_PTR(ptr_name) 相当于typeid(指针)
DYNAMIC_CAST(object_ptr,class_name) 相当于dynamic_castobject_ptr



  性能测试

  测试代码:

  这里使用相同次数的DYNAMIC_CAST和dynamic_cast进行对比测试,在VC6.0下编译运行,使用默认的Release编译配置选项。为了避免编译器优化导致的不公平测试结果,我在循环中加入了无意义的计数操作。

void main()
{
UObject* po=new UObject;
UView* pv=new UView;
UObject* pg=new UGraph;
int a,b,c,d;
a=b=c=d=0;
const int times=30000000;
cerr << "时间测试输出:" << endl;
cerr << "start my DYNAMIC_CAST at: " << time(NULL) << endl;
for(int i=0;i
if(DYNAMIC_CAST(po,UView)) a++; else a--;
if(DYNAMIC_CAST(pv,UView)) b++; else b--;
if(DYNAMIC_CAST(po,UGraph)) c++; else c--;
if(DYNAMIC_CAST(pg,UGraph)) d++; else d--;
}
cerr << "end my DYNAMIC_CAST at: " << time(NULL) << endl;
cerr << "start c++ dynamic_cast at: " << time(NULL) << endl;
for(i=0;i
if(dynamic_cast(po)) a++; else a--;
if(dynamic_cast(pv)) b++; else b--;
if(dynamic_cast(po)) c++; else c--;
if(dynamic_cast(pg)) d++; else d--;
}
cerr << "end c++ dynamic_cast at: " << time(NULL) << endl;
cerr << a << b << c << d << endl;
}

  运行结果:

start my DYNAMIC_CAST at: 1021512140
end my DYNAMIC_CAST at: 1021512145
start c++ dynamic_cast at: 1021512145
end c++ dynamic_cast at: 1021512160

  这是上述条件下的测试输出,我们可以看到,本文实现的这个精简RTTI方案运行DYNAMIC_CAST的时间开销只有dynamic_cast的1/3。为了得到更全面的数据,还进行了DEBUG编译配置选项下的测试。

  输出:

start my DYNAMIC_CAST at: 1021512041
end my DYNAMIC_CAST at: 1021512044
start c++ dynamic_cast at: 1021512044
end c++ dynamic_cast at: 1021512059

  这种情况下DYNAMIC_CAST运行速度要比dynamic_cast慢一倍左右。如果在Release编译配置选项下将UObject:: IsKindOf方法改成如下inline函数,我们将得到更让人兴奋的结果(DYNAMIC_CAST运行时间只有dynamic_cast的 1/5)。

inline bool UObject::IsKindOf(TypeInfo& cls)
{
for(TypeInfo* p=&(this->GetTypeInfo());p!=NULL;p=p->pBaseClass)
if(p==&cls) return true;
return false;
}

  输出:

start my DYNAMIC_CAST at: 1021512041
end my DYNAMIC_CAST at: 1021512044
start c++ dynamic_cast at: 1021512044
end c++ dynamic_cast at: 1021512059

  结论:

  由本文的实践可以得出结论,自己动手编码实现RTTI是简单可行的。这样的实现可以在编译器优秀的代码优化中表现出比dynamic_cast更好的性能,而且没有带来过多的存储开销。本文的RTTI以性能为主要设计目标,在实现上一定程度上受到了MFC的影响。适于嵌入式环境。
12月18日

关于C++的 explicit关键字

转自http://www.cppblog.com/tommy/archive/2006/02/05/3058.html

简而言之:explicit修饰的构造函数不能担任转换函数

这个 《ANSI/ISO C++ Professional Programmer's Handbook 》是这样说的

explicit Constructors
A constructor that takes a single argument is, by default, an implicit conversion operator, which converts its argument to
an object of its class (see also Chapter 3, "Operator Overloading"). Examine the following concrete example:
class string
{
private:
int size;
int capacity;
char *buff;
public:
string();
string(int size); // constructor and implicit conversion operator
string(const char *); // constructor and implicit conversion operator
~string();
};
Class string has three constructors: a default constructor, a constructor that takes int, and a constructor that
constructs a string from const char *. The second constructor is used to create an empty string object with an
initial preallocated buffer at the specified size. However, in the case of class string, the automatic conversion is
dubious. Converting an int into a string object doesn't make sense, although this is exactly what this constructor does.

Consider the following:
int main()
{
string s = "hello"; //OK, convert a C-string into a string object
int ns = 0;
s = 1; // 1 oops, programmer intended to write ns = 1,
}
In the expression s= 1;, the programmer simply mistyped the name of the variable ns, typing s instead. Normally,
the compiler detects the incompatible types and issues an error message. However, before ruling it out, the compiler first
searches for a user-defined conversion that allows this expression; indeed, it finds the constructor that takes int.
Consequently, the compiler interprets the expression s= 1; as if the programmer had written
s = string(1);
You might encounter a similar problem when calling a function that takes a string argument. The following example
can either be a cryptic coding style or simply a programmer's typographical error. However, due to the implicit
conversion constructor of class string, it will pass unnoticed:
int f(string s);
int main()
{
f(1); // without a an explicit constructor,
//this call is expanded into: f ( string(1) );
//was that intentional or merely a programmer's typo?
}
'In order to avoid such implicit conversions, a constructor that takes one argument needs to be declared explicit:
class string
{
//...
public:
explicit string(int size); // block implicit conversion
string(const char *); //implicit conversion
~string();
};
An explicit constructor does not behave as an implicit conversion operator, which enables the compiler to catch the
typographical error this time:
int main()
{
string s = "hello"; //OK, convert a C-string into a string object
int ns = 0;
s = 1; // compile time error ; this time the compiler catches the typo
}
Why aren't all constructors automatically declared explicit? Under some conditions, the automatic type conversion is
useful and well behaved. A good example of this is the third constructor of string:
string(const char *);

The implicit type conversion of const char * to a string object enables its users to write the following:
string s;
s = "Hello";
The compiler implicitly transforms this into
string s;
//pseudo C++ code:
s = string ("Hello"); //create a temporary and assign it to s
On the other hand, if you declare this constructor explicit, you have to use explicit type conversion:
class string
{
//...
public:
explicit string(const char *);
};
int main()
{
string s;
s = string("Hello"); //explicit conversion now required
return 0;
}
Extensive amounts of legacy C++ code rely on the implicit conversion of constructors. The C++ Standardization committee was aware of that. In order to not make existing code break, the implicit conversion was retained. However, a
new keyword, explicit, was introduced to the languageto enable the programmer to block the implicit conversion
when it is undesirable. As a rule, a constructor that can be invoked with a single argument needs to be declared
explicit. When the implicit type conversion is intentional and well behaved, the constructor can be used as an
implicit conversion operator.

static_cast<>揭密

作者:Sam NG 译者:小刀人


原文链接:What static_cast<> is actually doing 转自 http://www.vckbase.com/document/viewdoc/?id=653

本文讨论static_cast<> 和 reinterpret_cast<>。

介绍
大多程序员在学C++前都学过C,并且习惯于C风格(类型)转换。当写C++(程序)时,有时候我们在使用static_cast<>和reinterpret_cast<>时可能会有点模糊。在本文中,我将说明static_cast<>实际上做了什么,并且指出一些将会导致错误的情况。

泛型(Generic Types)

        float f = 12.3;
float* pf = &f;

// static cast<>
// 成功编译, n = 12
int n = static_cast<int>(f);
// 错误,指向的类型是无关的(译注:即指针变量pf是float类型,现在要被转换为int类型) //int* pn = static_cast<int*>(pf);
//成功编译
void* pv = static_cast<void*>(pf);
//成功编译, 但是 *pn2是无意义的内存(rubbish)
int* pn2 = static_cast<int*>(pv);

// reinterpret_cast<>
//错误,编译器知道你应该调用static_cast<>
//int i = reinterpret_cast<int>(f);
//成功编译, 但是 *pn 实际上是无意义的内存,和 *pn2一样
int* pi = reinterpret_cast<int*>(pf);

简而言之,static_cast<> 将尝试转换,举例来说,如float-到-integer,而reinterpret_cast<>简单改变编译器的意图重新考虑那个对象作为另一类型。

指针类型(Pointer Types)

指针转换有点复杂,我们将在本文的剩余部分使用下面的类:
class CBaseX
{
public:
int x;
CBaseX() { x = 10; }
void foo() { printf("CBaseX::foo() x=%d\n", x); }
};

class CBaseY
{
public:
int y;
int* py;
CBaseY() { y = 20; py = &y; }
void bar() { printf("CBaseY::bar() y=%d, *py=%d\n", y, *py); }
};

class CDerived : public CBaseX, public CBaseY
{
public:
int z;
};

情况1:两个无关的类之间的转换



      // Convert between CBaseX* and CBaseY*
// CBaseX* 和 CBaseY*之间的转换
CBaseX* pX = new CBaseX();
// Error, types pointed to are unrelated
// 错误, 类型指向是无关的
// CBaseY* pY1 = static_cast<CBaseY*>(pX);
// Compile OK, but pY2 is not CBaseX
// 成功编译, 但是 pY2 不是CBaseX
CBaseY* pY2 = reinterpret_cast<CBaseY*>(pX);
// System crash!!
// 系统崩溃!!
// pY2->bar();
正如我们在泛型例子中所认识到的,如果你尝试转换一个对象到另一个无关的类static_cast<>将失败,而reinterpret_cast<>就总是成功"欺骗"编译器:那个对象就是那个无关类。

情况2:转换到相关的类
      1. CDerived* pD = new CDerived();

2. printf("CDerived* pD = %x\n", (int)pD);
3.
4. // static_cast<> CDerived* -> CBaseY* -> CDerived*
//成功编译,隐式static_cast<>转换
5. CBaseY* pY1 = pD;
6. printf("CBaseY* pY1 = %x\n", (int)pY1);
// 成功编译, 现在 pD1 = pD
7. CDerived* pD1 = static_cast<CDerived*>(pY1);
8. printf("CDerived* pD1 = %x\n", (int)pD1);
9.
10. // reinterpret_cast
// 成功编译, 但是 pY2 不是 CBaseY*
11. CBaseY* pY2 = reinterpret_cast<CBaseY*>(pD);
12. printf("CBaseY* pY2 = %x\n", (int)pY2);
13.
14. // 无关的 static_cast<>
15. CBaseY* pY3 = new CBaseY();
16. printf("CBaseY* pY3 = %x\n", (int)pY3);
// 成功编译,尽管 pY3 只是一个 "新 CBaseY()"
17. CDerived* pD3 = static_cast<CDerived*>(pY3);
18. printf("CDerived* pD3 = %x\n", (int)pD3);
      ---------------------- 输出 ---------------------------
CDerived* pD = 392fb8
CBaseY* pY1 = 392fbc
CDerived* pD1 = 392fb8
CBaseY* pY2 = 392fb8
CBaseY* pY3 = 390ff0
CDerived* pD3 = 390fec

注意:在将CDerived*用隐式 static_cast<>转换到CBaseY*(第5行)时,结果是(指向)CDerived*(的指针向后) 偏移了4(个字节)(译注:4为int类型在内存中所占字节数)。为了知道static_cast<> 实际如何,我们不得不要来看一下CDerived的内存布局。

CDerived的内存布局(Memory Layout)



如图所示,CDerived的内存布局包括两个对象,CBaseX 和 CBaseY,编译器也知道这一点。因此,当你将CDerived* 转换到 CBaseY*时,它给指针添加4个字节,同时当你将CBaseY*转换到CDerived*时,它给指针减去4。然而,甚至它即便不是一个CDerived你也可以这样做。
当然,这个问题只在如果你做了多继承时发生。在你将CDerived转换 到 CBaseX时static_cast<> 和 reinterpret_cast<>是没有区别的。

情况3:void*之间的向前和向后转换

因为任何指针可以被转换到void*,而void*可以被向后转换到任何指针(对于static_cast<> 和 reinterpret_cast<>转换都可以这样做),如果没有小心处理的话错误可能发生。

    CDerived* pD = new CDerived();
printf("CDerived* pD = %x\n", (int)pD);

CBaseY* pY = pD; // 成功编译, pY = pD + 4
printf("CBaseY* pY = %x\n", (int)pY);

void* pV1 = pY; //成功编译, pV1 = pY
printf("void* pV1 = %x\n", (int)pV1);

// pD2 = pY, 但是我们预期 pD2 = pY - 4
CDerived* pD2 = static_cast<CDerived*>(pV1);
printf("CDerived* pD2 = %x\n", (int)pD2);
// 系统崩溃
// pD2->bar();

        ---------------------- 输出 ---------------------------
CDerived* pD = 392fb8
CBaseY* pY = 392fbc
void* pV1 = 392fbc
CDerived* pD2 = 392fbc

一旦我们已经转换指针为void*,我们就不能轻易将其转换回原类。在上面的例子中,从一个void* 返回CDerived*的唯一方法是将其转换为CBaseY*然后再转换为CDerived*。
但是如果我们不能确定它是CBaseY* 还是 CDerived*,这时我们不得不用dynamic_cast<> 或typeid[2]。

注释:
1. dynamic_cast<>,从另一方面来说,可以防止一个泛型CBaseY* 被转换到CDerived*。
2. dynamic_cast<>需要类成为多态,即包括"虚"函数,并因此而不能成为void*。
参考:
1. [MSDN] C++ Language Reference -- Casting
2. Nishant Sivakumar, Casting Basics - Use C++ casts in your VC++.NET programs
3. Juan Soulie, C++ Language Tutorial: Type Casting