Home

test-o-matic

test-o-matic is a unit testing library for C++. It is not a framework; it is designed to let each person write and organize tests in their own way.

Goals

  • free for use in commercial, closed-source and open-source software
  • small (approx. 500 lines of code in 1 source file and 1 header file)
  • quick and easy to use
  • quick and easy to build
  • quick and easy to modify
  • portable
  • flexible and extensible
  • unintrusive
  • independent of any 3rd party code

Below you will find all that's needed to get started with test-o-matic. For information and examples relating to customizing and adapting the library further, please visit the Cookbook.

Writing tests

If tests are hard to write, they don't get written. So test-o-matic minimizes boilerplate code as far as possible.

For extremely quick-and-dirty tests, you can use the macros provided by test-o-matic in your main() function:

#include <test_o_matic.hpp>

int main()
{
    CHECK(1 == 1);
    THROWS(throw std::runtime_error("oops"), std::exception);
    // ...

    // To print a summary:
    return get_local_tom_logger().summary(std::cout);
}

However, as the number of tests grows, this approach soon becomes unwieldy. It's best to try to keep tests isolated so that they don't step on each other's toes.

Here's the code needed for an isolated test, assuming we're testing a bignum class:

TEST("bignum equality")
{
    bignum one(1), three(3), five(5);
    CHECK(five == five);
    CHECK(!(one == three));
}

This defines a test with the name "bignum equality". The CHECK() macro tests that the expression given as an argument evaluates to true.

There is also a REQUIRE() macro, that is like CHECK() but will abort the test if it fails; CHECK() will proceed to the next statement in the code regardless of whether it passed or failed.

The ABORT() macro can be called at any time to quit a test. You can also simply use a return statement, but that won't generate any output.

The TRY() macro checks that an expression doesn't throw an exception.

TEST("bignum construction")
{
    TRY(bignum("99999999999999999999999999999999"));
}

The THROWS() macro checks to see if an expression throws a particular type of exception. Here we check that division by zero throws a divide_by_zero object.

TEST("bignum division")
{
    THROWS(bignum(1) / bignum(0), divide_by_zero);
}

I've found that these macros cover all the cases I've ever been interested in, but it is also easy to write your own macros to deliver arbitrary messages to the logging system.

Writing fixtures

Sometimes you may find that you end up writing a lot of initialization code that is repeated among tests. This can be mitigated by writing what's known as a fixture.

In test-o-matic, a fixture is simply a class or struct with a public zero-argument constructor. If we find that we're creating lots of bignum tests that use objects called zero, one and huge, we could do this to save some repetition:

struct common_bignums
{
    bignum zero;
    bignum one;
    bignum huge;

    common_bignums() : zero(0), one(1), huge("9999999999999999999999") { }

    // you can add a destructor too, if you need to tear down anything
};

TESTFIX("bignum multiplication", common_bignums)
{
    // The member variables of a fixture are made available to the test:
    CHECK(one * one == one);
    CHECK(zero * one == zero);
    TRY(huge * huge);
    //...
}

Such a fixture class can be used in any number of tests. Note that we used the TESTFIX() macro here, rather than TEST().

Running your tests

Here's how to run all the tests we just created:

namespace tom { using namespace test_o_matic; }

int main()
{
    const bool verbose = true;
    tom::simple_logger lgr(std::cout, verbose);
    tom::runner rnr;

    for (const tom::test *t = tom::first_test(); t; t = t->next)
        tom::run_test(*t, lgr, rnr);

    return lgr.summary(std::cout);
}

Here's the output from the tests that exercise test-o-matic itself.

Note how it's possible to iterate over all the tests. Each test object has a bunch of fields:

  • name - the name of the test e.g. "bignum construction"
  • file - the name of the C++ source file in which the test is defined
  • line - the line number at which the test definition starts
  • date - the date on which the test was compiled e.g. "Apr 15 1982"
  • time - the time at which the test was compiled e.g. "16:30:10"
  • next - a pointer to the next test

You can use this information to filter out the tests you're not interested in, or collect tests in to suites of some kind. Perhaps you only want to run all tests that were compiled today, or within the last 10 minutes?

A logger object is used to print information about events and gather basic statistics as tests run. A runner object is used to call each test. Both are entirely customizable.

Organising your tests

There are no restrictions on where you should put your tests. I tend to collect the tests for a given component in to a file and put the test-execution loop in its own file, too.

timer_tests.cpp // tests for timer
animator_tests.cpp // tests for animator
interpolator_tests.cpp // tests for interpolator
...
run.cpp // code to run the tests

But for a quick fix you can put the test definitions and running code all in a single file.

Another interesting strategy is to have all tests for a given class in their own file as before, but now compile them in to a shared libraries (.dll/.so/.dylib). Each shared library will export the first_test() and run_test() functions, allowing you to dynamically load and run tests using functions such as

  • LoadLibrary/GetProcAddress on Windows
  • dlopen/dlsym on UNIX and Mac

So rather than having lots of little test applications, you instead have lots of little test libraries and a single program to run them. Perhaps the program can descend a directory hierarchy looking for tests and/or test libraries that match a user-specified pattern?

Such a program isn't provided by test-o-matic as I'd like to keep the library super-lean. Perhaps I'll start another project at some point to provide a library or program for this purpose...

More information

Updated

Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.