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.
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.
Grrrr, WordPress has completely garabled all of the angle brackets from my comments.
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.
Charles emailed the ungarbled version to me:
What about just plain old this?:
Where TEST_ASSERT_CLS does the obvious thing.
Stringificizationalism seems the most reliable way to get the class name here.