Miscellaneous Topics

Arguments to Constructors

Sometimes, you want to write several tests that differ only in the values of some parameters. In this case, you can pass the parameters as arguments to its constructor; you can specify their values as non-template arguments when adding the test to the suite. Here's an example:

    class Square {
    public:
      Square(int toSquare, int expected)
        : toSquare_(toSquare), expected_(expected) {
      }

      void run() {
        ASSERT_EQUALS(expected_, toSquare * toSquare);
      }

    private:
      const int toSquare_;
      const int expected_;
    };

    class All : public UnitTest::Suite {
    public:
      All() {
        add<Square>(0, 0);
        add<Square>(1, 1);
        add<Square>(2, 4);
      }
    };

This can get a little verbose (as in this example); if that happens, you can sometimes make the test class itself a template class, with the parameters template arguments instead of constructor arguments.

-list and -debug

As we mentioned before, if you only want to run tests from some of the suites, you can pass the names of the suites as arguments to your test programs. If you've forgotten the names of the suites, however, you can pass the argument -list to your test program. It will print out the names of your suites and exit.

If, for some reason, you need to know when each test starts and finishes, you can pass the argument -debug to your test program. It will print out the name of each test as it gets constructed, run, and destroyed.

An Empty Suite Gives a Single Pass

If you add an suite which doesn't contain any tests, your program will report one pass and no failures. The reason for this is that each suite counts as a test.

Similarly, if you add a suite which contains one failing test, then your program will report one pass and one failure. The failure is from the failing test; the pass is from the suite. The suite passed because it did its job, namely to run the tests contained therein; the fact that the test failed doesn't mean that the suite failed.

If, for some reason, the suite has a problem in its constructor or destructor, however, the suite will be marked as failing.

Signals During Tests

If your test causes certain signals (seg fault, divide by zero, abort) to be raised, then the framework will catch that signal print out an appropriate message, and mark the test as having failed. It will in addition print out a backtrace of where the signal occurred (at least under Linux). That backtrace isn't always very helpful so, much of the time, it's easier to simply run your test program under GDB.

In this situation, the test framework will attempt to clean up your test and continue as normal, but it will sometimes be unable to do so. In particular, the recovery mechanism is handled via setjmp/longjmp instead of exceptions, so not all objects' destructors may be called normally. It will attempt to call your test's destructor, however.

Radix

If an ASSERT_EQUALS fails, it prints out the actual and expected values. If the values in question are integers, it will print them out in decimal by default. If you wish to see the values in hex, you can do the following:

    UnitTest::radix(UnitTest::HEX);

Breakpoints on Assertions

Sometimes, when an assertion fails, you want to be able to poke at the environment in a debugger. Behind the scenes, assertions are implemented via exceptions; if you're lucky, doing catch throw in GDB will let you know when the corresponding exception is thrown.

If your code throws a lot of exceptions, however, it might be hard to pick out the right one. In this case, the natural thing to do would be to set a breakpoint on the assertion exception class's constructor. Unfortunately, as of this writing, setting breakpoints in constructors doesn't reliably work in GDB. To work around this, we've provided a function UnitTest::AssertionFailure::triggered which the constructor calls; you can set a breakpoint there instead.

FAIL

Assertions are used to detect problems discovered by your test's run() function. You can also use them in your test's constructor, if for some reason you want to do that as a sanity check. (It's usually a sign of bad design, however.)

Sometimes, however, you want to signal that a problem occurred in a test's or destructor. (Normally, this is a sign of problems in your test design; the main situation where this happens naturally is in the destructor of mixin classes.) In this situation, using assertions is an extremely bad idea: they're implemented as exceptions, and, for various good reasons, throwing exceptions in a destructor is very bad form.

If you detect a problem in a destructor, therefore, you should instead call FAIL(), passing it an appropriate string as an argument. The rest of the destructor will run as normal (as opposed to an assertion failure, which immediately halts the run() function), but the test will be marked as having failed, even if no assertion was triggered. (If both an assertion and a FAIL() trigger, however, only the assertion message will be printed.)

You can also call FAIL() during the running of a test. (Or during the constructor, but I can't think of a reason why you'd want to do that.) There's rarely a need to do so; one case when it could come in handy is when testing multithreaded code. You can't do an assertion in a different thread from the one in which run() is being called, but you can call FAIL() from any thread. (The implementation isn't thread-safe, but you're unlikely to have multiple threads simultaneously calling FAIL() anyways.)

Using RAII Classes

When using this framework, it's helpful to not only follow good TDD style, as practiced in other languages, but also to follow good C++ style. In particular, the "Resource Acquisition Is Initialization" design pattern is very helpful. If several tests within a suite want the same sort of setup and teardown, you can pull that to a base class's constructor and destructor. If several tests across different suites want to do a similar sort of setup or teardown, however, that's a sign that you should abstract out a helper class (or an "RAII class") whose only job is to provide a constructor and destructor to do the setup and teardown. Your different tests in different suites can then all have member variables whose type is that RAII class.

In general (not just when writing tests, but when writing any sort of C++ code), you should look on destructors with suspicion: whenever a destructor is doing something, you should ask yourself whether that functionality should be moved out into an RAII class. For example, it's rarely good form for a destructor to call delete - instead, your class should have a member variable whose type is std::auto_ptr (or some other type of smart pointer). Doing so can simplify your code significantly, and makes it much easier to write robust, exception-safe code.


david carlton <carlton@bactrian.org>