...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
Today is a momentous day - first day of new year. Today I am going to start a new life. I am going to stop eating a greasy food, start attending a fitness club and ... today I am going to test programs I am writing. I can start right after the last line of a program is completed or, even better, I can write tests while I am coding. And maybe next time I will write tests before the coding, during the design stage. I have read a lot of literature on how to write the tests, I have the unit test framework in hand and an idea of new class. So let's get started.
Let say I want to encapsulate an unchangeable C character buffer with a
length into the simple class const_string
.
Rationale: a string class that does not allocate a memory and provide a
convenient read-only access to the pre-allocated character buffer. I will
probably want const_string
to have an interface similar to the class std::string. What will I do first?
In my new life I will start with writing a test module for future class
const_string
. It will look
like this:
#define BOOST_TEST_MODULE
const_string test
#include <boost/test/unit_test.hpp>
Now I can compile it and link with the unit test framework. Done! I have a working test program. It is empty, so when I run the program it produces following output:
*** No errors detected
Well, now it could be a good time to start a work on const_string
.
First thing I imagine would be good to have is a constructors and trivial
access methods. So my class initial version looks like this:
class const_string { public: // Constructors const_string(); const_string( std::string const& s ) const_string( char const* s ); const_string( char const* s, size_t length ); const_string( char const* begin, char const* end ); // Access methods char const* data() const; size_t length() const; bool is_empty() const; // ... };
Now I am able to write a first test case - constructors testing - and add it to a test suite. My test program became to look like this:
#defineBOOST_TEST_MODULE
const_string test #include <boost/test/unit_test.hpp>BOOST_AUTO_TEST_CASE
( constructors_test ) { const_string cs0( "" ); // 1 //BOOST_TEST
( cs0.length() == (size_t)0 );BOOST_TEST
( cs0.is_empty() ); const_string cs01( NULL ); // 2 //BOOST_TEST
( cs01.length() == (size_t)0 );BOOST_TEST
( cs01.is_empty() ); const_string cs1( "test_string" ); // 3 //BOOST_TEST
( std::strcmp( cs1.data(), "test_string" ) == 0 );BOOST_TEST
( cs1.length() == std::strlen("test_string") ); std::string s( "test_string" ); // 4 // const_string cs2( s );BOOST_TEST
( std::strcmp( cs2.data(), "test_string" ) == 0 ); const_string cs3( cs1 ); // 5 //BOOST_TEST
( std::strcmp( cs3.data(), "test_string" ) == 0 ); const_string cs4( "test_string", 4 ); // 6 //BOOST_TEST
( std::strncmp( cs4.data(), "test", cs4.length() ) == 0 ); const_string cs5( s.data(), s.data() + s.length() ); // 7 //BOOST_TEST
( std::strncmp( cs5.data(), "test_string", cs5.length() ) == 0 ); const_string cs_array[] = { "str1", "str2" }; // 8 //BOOST_TEST
( cs_array[0] == "str1" );BOOST_TEST
( cs_array[1] == "str2" ); }
The constructors_test test case is intended to check a simple feature of
the class const_string
:
an ability to construct itself properly based on different arguments. To
test this feature I am using such characteristics of constructed object
as a data it contains and a length. The specification of the class const_string
does not contain any expected
failures, so, though the constructor can fail if I would pass a pointer
to an invalid memory, error check control is not performed (can't require
what was not promised :-)). But for any valid input it should work. So
I am trying to check a construction for an empty string (1), a NULL string
(2) a regular C string(3), an STL string(4), a copy construction(5) and
so on. Well, after fixing all the errors in the implementation (do you
write programs without errors from scratch?) I am able to pass this test
case and the unit test framework gives me the following report:
Running 1 test case... *** No errors detected
Encouraged I am moving on and adding more access methods:
class const_string { public: //... char operator[]( size_t index ) const; char at( size_t index ) const; //... };
I added the new feature - I need a new test case to check it. As a result my test suite became to look like this:
#defineBOOST_TEST_MODULE
const_string test #include <boost/test/unit_test.hpp>BOOST_AUTO_TEST_CASE
( constructors_test ) { //... }BOOST_AUTO_TEST_CASE
( data_access_test ) { const_string cs1( "test_string" ); // 1 //BOOST_TEST
( cs1[(size_t)0] == 't' );BOOST_TEST
( cs1[(size_t)4] == '_' );BOOST_TEST
( cs1[cs1.length()-1] == 'g' );BOOST_TEST
( cs1[(size_t)0] == cs1.at( 0 ) ); // 2 //BOOST_TEST
( cs1[(size_t)2] == cs1.at( 5 ) );BOOST_TEST
( cs1.at( cs1.length() - 1 ) == 'g' ); BOOST_CHECK_THROW( cs1.at( cs1.length() ), std::out_of_range ); // 3 // }
In the data_access_test test case I am trying to check the class const_string
character access correctness.
While tests (1) checks valid access using const_string::operator[]
and test (2) checks valid access using
method const_string::at()
,
there is one more thing to test. The specification of the method const_string::at()
contains validation for the out of bound access. That was test (3) is intended
to do: check that the validation is working. A testing of a validation
and error handling code is an important part of a unit testing and should
not be left for a production stage. The data_access_test test case passed
and I am ready for the next step.
Continuing my effort I am able to complete class const_string
(see Listing 1 const_string.hpp
)
and testing module for it (see Listing
2 const_string_test.cpp
) that is checking all
features that are presented in the class const_string
specification.
Well, I am step closer to fulfilling my new year resolution (we should see about this fitness club sometime next ...). What about you? Your testing habits could be a little different. You could start with a class/library development and then at some point start writing test cases on feature basis. Or you can, given a detailed specification for the future product, including expected interfaces, immediately start with writing all test cases (or it could be a different person, while you working on implementation at the same time). In any case you should not have any problems to use facilities provided by the Boost.Test unit test framework and, let me hope, be able to write a stable, bulletproof code. And what is even more important is your confidence in an ability to make changes of any complexity without involving a lengthy regression testing of your whole product. Your test module and the unit test framework will stay behind your back to help you with any occasional errors.