Unit testing value classes with templates Tuesday 13th May 2008
Classes with value semantics are a very important, err, class of classes. Frequently used as the building blocks for other code, it is important that they are tested and behave as expected.
The following function templates are designed to make testing the basic properties of value classes as simple as possible. They are designed to be used with a simple C++ testing framework such as hshgtest.
The tests are designed to test classes that are default constructible and have a number of properties each of which affect the value of the class. Changing any of the properties away from its default value should change the value of the class. Copy construction and assignment should preserve the value of the class in the expected way and value classes may have a serialization mechanism which should preserve the value of the class through an intermediate ‘flat’ form, typically a byte sequence.
To test a particular class, it is required to specialize the following function template:
template< class T > void FillTestVector( std::vector< T >& vec );
The implementation should populate vec with any number of class instances each of which must differ from a default constructed instance of the class. Typically one would create as many instances as the class has distinct properties and make each instance differ from a default constructed instance by perturbing just one property from its default value. This maximizes the tests’ coverage.
The “Not Equal” Test
The first set of tests is a basic sanity check of operators == and != and of the set of values chosen for the FillTestVector specialization. Instances should compare equal to themselves and non-default instances should compare unequal to default constructed instances.
template< class T > void NeqTest() { typedef typename std::vector< T >::iterator iterator; std::vector< T > vec; FillTestVector( vec ); T def; HSHG_ASSERT( def == def ); HSHG_ASSERT( !(def != def) ); HSHG_ASSERT( !vec.empty() ); for( iterator i = vec.begin(); i != vec.end(); ++i ) { HSHG_ASSERT( def != *i ); HSHG_ASSERT( !(def == *i) ); HSHG_ASSERT( *i == *i ); HSHG_ASSERT( !(*i != *i) ); } }
The “CopyConstruct” Test
As the name implies… tests the copy constructor.
template< class T > void CopyConstructTest() { typedef typename std::vector< T >::iterator iterator; std::vector< T > vec; FillTestVector( vec ); HSHG_ASSERT( !vec.empty() ); for( iterator i = vec.begin(); i != vec.end(); ++i ) { T copy( *i ); HSHG_ASSERT( copy == *i ); } }
The “Assignment” Test
As the name implies… tests the assignment operator.
template< class T > void AssignmentTest() { typedef typename std::vector< T >::iterator iterator; std::vector< T > vec; FillTestVector( vec ); HSHG_ASSERT( !vec.empty() ); for( iterator i = vec.begin(); i != vec.end(); ++i ) { T def; def = *i; HSHG_ASSERT( def == *i ); } }
The Serialization Test
This test is a little more flexible. There are numerous ways of streaming class and numerous representations of a ‘flat’ serialized class. The serialization and deserialization functions are abstracted into two class templates, Freezer and Thawer.
template< class S, class T > struct Freezer; template< class S, class T > struct Thawer; template< class S, class T > S Freeze( const T& t ) { return Freezer< S, T >::freeze( t ); } template< class S, class T > T Thaw( const S& s ) { return Thawer< S, T >::thaw( s ); } template< class S, class T > void StreamTest() { typedef typename std::vector< T >::iterator iterator; std::vector< T > vec; FillTestVector( vec ); HSHG_ASSERT( !vec.empty() ); for( iterator i = vec.begin(); i != vec.end(); ++i ) { T deiced( Thaw< S, T >( Freeze< S, T >( *i ) ) ); HSHG_ASSERT( deiced == *i ); } }
The two function templates, Freeze and Thaw, serialize and deserialize the class under test (T) though an intermediate flat data class (S) using the Freezer and Thawer class templates. This test requires a specialization of Freezer and Thawer to specify how the serialization is performed.
Here is a partial specialization which uses standard insertion and extraction operators with std::stringstream classes to serialize the class under test to and from a std::string.
template< class T > struct Freezer< std::string, T > { static std::string freeze( const T& t) { std::ostringstream r; r << t; return r.str(); } }; template< class T > struct Thawer< std::string, T > { static T thaw( const std::string& s) { std::istringstream r( s ); T t; r >> t; return t; } };
Putting it all together
For convenience, we might want to put these all together so that testing a new class is as simple as possible.
template< class T > void TestAll() { NeqTest< T >(); CopyConstructTest< T >(); AssignmentTest< T >(); StreamTest< std::string, T >(); }
Now adding a new class to be tested is as simple as added a specialization of FillTestVector and adding a TestAll() test call.
struct MyNewValue { MyNewValue() : _ivy(-1) {} int _ivy; std::string _leaf; }; template<> void FillTestVector( std::vector< T >& vec ) { vec.resize(2); vec[0]._ivy = 42; vec[1]._leaf = "non-default string"; } // Add TestAll< MyNewValue >() to test calls.
Testing a new property is as simple as adding more test values to the test vector in the FillTestVector specialization. Testing that the property contributes to the equality operator, that the property is appropriately preserved in copy construction, assignment and serialization happens automatically.