Search code examples
c++templatescatch-unit-test

Test C++ template class using Catch framework


I'm looking for a good way to use Catch to test a templated class. I have something that almost works:

#define RUN_ALL(fn, params)  \
fn<uint8_t, bool>(params);  \
fn<uint8_t, char>(params);  \
fn<uint16_t, bool>(params); \
fn<uint16_t, char>(params); \
fn<uint32_t, bool>(params); \
fn<uint32_t, char>(params); \
fn<uint64_t, bool>(params); \
fn<uint64_t, char>(params);

template<typename A, typename B>
void test_number_one() {
   REQUIRE(...)
} 

TEST_CASE("Foo::Foo() works nicely", "[SmallGraph]") {
  RUN_ALL(test_number_one)
}

This setup will run only until the first failure, which is fine because it is highly likely that all 8 cases will fail the same way. However, it would be nice to know which set of template arguments are in use when a failure occurs. My idea is to do this:

#define RUN_ALL_P(fn, params)  \
INFO("Testing <uint8_t, bool>"); \
fn<uint8_t, bool>(params);  \
INFO("Testing <uint8_t, char>"); \
fn<uint8_t, char>(params);  \
INFO("Testing <uint16_t, bool>"); \
fn<uint16_t, bool>(params); \
...

However, I can't use more than one INFO in RUN_ALL because doing so generates code with a duplicate identifier.

FOO.cpp:270:3: error: redefinition of 'scopedMessage270'
  RUN_ALL(test_number_one);

(RUN_ALL(test_number_one) appears on line 270.)

Any ideas for a workaround that doesn't require all test functions to the same signature?

(I would also welcome pointers to articles about testing template code using CATCH, as well as suggestions on how to search for such articles without getting a bunch of results about general exception handling -- i.e., try/catch.)


Solution

  • The problem with your macro is that, when it is expanded, it is expanded to a single line. Although I don't know your test framework in use, it is obvious that the macro does something comparable to this:

    struct M { M(char* msg) { puts(msg); } }; // just an example class...
    #define INFO(m) M scopedMessage##__line__(msg)
    

    So you will get multiple scopedMessage270 instances, if you use your macro RUN_ALL at line 270...

    You could work around the problem by replacing the macro with a template. Unfortunately, you cannot use this with template functions, so you will have to make your test cases template classes, too:

    template <template <typename T, typename TT > class Test >
    struct All
    {
        template <typename ... Parameters>
        static void run(Parameters ... parameters)
        {
            Test<uint8_t, bool>::run(parameters ...);
            Test<uint8_t, char>::run(parameters ...);
            Test<uint16_t, bool>::run(parameters ...);
            Test<uint16_t, char>::run(parameters ...);
            Test<uint32_t, bool>::run(parameters ...);
            Test<uint32_t, char>::run(parameters ...);
            Test<uint64_t, bool>::run(parameters ...);
            Test<uint64_t, char>::run(parameters ...);
        }
    };
    
    template<typename A, typename B>
    struct test_number_one
    {
        static void run()
        {
            // log test name
            // run the test
        }
    };
    template<typename A, typename B>
    struct test_number_two
    {
        static void run(int n)
        {
            // log test name and parameter value
            // run the test
        }
    };
    
    int main(int argc, char* argv[])
    {
        All<test_number_one>::run();
        All<test_number_two>::run(12);
        All<test_number_two>::run(10);
    }
    

    As now, within the template, all code lines remain on separate lines, you can place in between any logging just as you like:

    template <typename ... Parameters>
    static void run(Parameters ... parameters)
    {
        INFO("uint8_t, bool");
        Test<uint8_t, bool>::run(parameters ...);
        INFO("uint8_t, char");
        Test<uint8_t, char>::run(parameters ...);
    // ...