Templated test code?

At work at the moment, as part of an initiative to get with the 21st century, we are waking up to testing our code.

Thus, I am writing a lot of unit tests for old code, which can be soul-destroyingly repetitive and very pointless-feeling (even though really I do see a great value in the end result – tested code is refactorable code).

Often, tests have a lot in common with each other, so it feels right to reduce code repetition, and factor things into functions etc. The Right Way of doing this is to leave your tests as straightforward as possible, with preferably no code branches at all, just declarative statements.

Contemplating writing unit tests for the same method on 20+ very similar classes, using a template function “feels” right, for normal code values of “feel”. However, for test code, maybe it’s wrong?

My question is: is it ok to write a test function like this?:

void test_all_thingies()
{
    test_One_Thingy<Thingy1>();
    test_One_Thingy<Thingy2>();
    test_One_Thingy<Thingy3>();
    test_One_Thingy<Thingy4>();
}

template< class T >
void test_One_Thingy()
{
    T thingy;
    thingy.doSomething();
    TEST_ASSERT( thingy.isSomething() );
}

Worse still, is this ok?

void test_all_thingies()
{
    test_One_Thingy<Thingy1>( "Thingy1 expected output" );
    test_One_Thingy<Thingy2>( "Thingy2 expected output" );
    test_One_Thingy<Thingy3>( "Thingy3 expected output" );
    test_One_Thingy<Thingy4>( "Thingy4 expected output" );
}

template< class T >
void test_One_Thingy( std::string expected_output )
{
    T thingy;
    thingy.doSomething();
    TEST_ASSERT( thingy.getOutput() == expected_output );
}

Reasons for: otherwise I’m going to be writing huge amounts of copy-pasted code (unless someone can suggest a better way?).

Reasons against: how clear is it going to be which class failed the test when it fails?

Update: fixed unescaped diagonal brackets.

7 thoughts on “Templated test code?”

  1. I presume a clerical error has left out the template parameter specification on the test_One_Thingy function calls.

    ‘Reasons against’ can be rebutted fairly fast. The thing with templates is that they are ‘real code’, so you can’t use any preprocessor code in the template function to distinguish different instantiations.

    Assuming that TEST_ASSERT is a macro that makes use of __FILE__ and __LINE__ for its helpful diagnostic, the obvious way is to make a more configurable macro which does essentially the same thing, but allows you to specify the file and line. For example, make TEST_ASSERT_FL macro so that these two lines are equivalent.
    TEST_ASSERT_FL( x, __FILE__, __LINE__ )
    TEST_ASSERT( x )

    Then, change the signature of test_One_Thingy as follows.
    template void test_One_Thingy( const std::string& ex, const char* file, unsigned int line )

    And have it use the new macro.
    TEST_ASSERT_FL( thingy.getOutput() == expected_output, file, line );

    Then create a new macro.
    #defined TEST_ONE_THINGY( cls, expect ) test_One_Thingy( expect, __FILE__, __LINE__ )

    void test_all_thingies()
    {
    TEST_ONE_THINGY( ClassA, “Thingy1 expected output” );
    TEST_ONE_THINGY( ClassB, “Thingy2 expected output” );
    TEST_ONE_THINGY( ClassC, “Thingy3 expected output” );
    TEST_ONE_THINGY( ClassD, “Thingy4 expected output” );
    }

    File and line now give you which class caused the failure, at the expense of not telling you the file and line of the template code where the actual test is. (This may not matter much if the template code only has a single assert macro.)

    Now the funky way :) .

    Assuming you have a TestAssert underlying function on which the TEST_ASSERT macro is built…

    template
    inline std::string SuffixType( const std::string& in )
    {
    std::ostringstream s;
    s ( #x ).c_str(), __FILE__, __LINE__)

    template
    void test_One_Thingy( const std::string& expected_output )
    {
    T thingy;
    thingy.doSomething();
    TEST_ASSERT_TEMPL( T, thingy.getOutput() == expected_output );
    }

    Warning! The type suffix is not guaranteed to be human readable.

  2. Grrrr, WordPress has completely garabled all of the angle brackets from my comments.

  3. I think I get the idea, and it certainly helps us tell which test fails, but it still feels a little complicated for test code, which I should have added as a further reason against.

  4. Charles emailed the ungarbled version to me:

    #include "hshgtest.h"
    #include <iostream>
    #include <sstream>
    
    struct ClsA
    {
    	const char* str() const { return "My name is A"; }
    };
    
    struct ClsB
    {
    	const char* str() const { return "My name is B"; }
    };
    
    struct ClsC
    {
    	const char* str() const { return "My Name is C"; }
    };
    
    struct ClsD
    {
    	const char* str() const { return "My name is D"; }
    };
    
    namespace
    {
    
    template< class T >
    inline std::string SuffixType( const std::string& in )
    {
    	std::ostringstream s;
    	s << in << " : " << typeid( T ).name();
    	return s.str();
    }
    
    #define TEST_ASSERT_TEMPL( t, x ) HSHGTest::TestAssert( x, SuffixType< t >( #x ).c_str(), __FILE__, __LINE__)
    
    template< class T >
    void ClassTest( const std::string& expect )
    {
    	T test;
    	TEST_ASSERT_TEMPL( T, expect == test.str() );
    }
    
    void testall()
    {
    	ClassTest< ClsA >( "My name is A" );
    	ClassTest< ClsB >( "My name is B" );
    	ClassTest< ClsC >( "My name is C" );
    	ClassTest< ClsD >( "My name is D" );
    }
    
    }
    
    HSHG_BEGIN_TESTS
    HSHG_TEST_ENTRY( testall )
    HSHG_END_TESTS
    
    HSHG_TEST_MAIN
    
  5. What about just plain old this?:

    
    #define TEST_ONE_THINGY(CLS,OUTPUT) test_One_Thingy<CLS>( #CLS, OUTPUT );
    
    void test_all_thingies()
    {
        TEST_ONE_THINGY( Thingy1, "Thingy1 expected output" );
        TEST_ONE_THINGY( Thingy2, "Thingy2 expected output" );
        TEST_ONE_THINGY( Thingy3, "Thingy3 expected output" );
        TEST_ONE_THINGY( Thingy4, "Thingy4 expected output" );
    }
    
    template< class T >
    void test_One_Thingy( std::string class_name, std::string expected_output )
    {
        T thingy;
        thingy.doSomething();
        TEST_ASSERT_CLS( class_name, thingy.getOutput() == expected_output );
    }
    
    

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.