I second that.
But I think that given the shown code, an example of getting into such
mess, and out of it again, could be useful.
For this example I'll use the following i/o support functions:
-----------------------------------------------------------------------
<file "exception_util.hpp">
#pragma once
#include <stdexcept>
namespace my{
using std::runtime_error;
using std::string;
inline
auto fail( string const& message )
-> bool
{ throw runtime_error( message ); }
} // namespace my
</file>
-----------------------------------------------------------------------
<file "io_util.hpp">
#pragma once
#include "exception_util.hpp"
#include <iostream>
#include <string>
namespace my {
using std::cin;
using std::cout;
using std::ostream;
using std::getline;
using std::string;
inline
auto int_from_user()
-> int
{
string line;
getline( cin, line ) || fail( "Input failed" );
return stoi( line );
}
inline
auto int_from_user( string const prompt )
-> int
{
cout << prompt;
return int_from_user();
}
class Output_value_list
{
private:
ostream& stream_;
bool is_first_value_;
using This = Output_value_list;
auto operator=( This const& ) -> This& = delete;
public:
template< class Value >
void operator()( Value const& value )
{
if( not is_first_value_ ) { stream_ << ", "; }
stream_ << value;
is_first_value_ = false;
}
Output_value_list( ostream& stream )
: stream_( stream )
, is_first_value_( true )
{}
};
} // namespace my
</file>
*** STEP 0
We write a little program that outputs a mysterious (or not so very
mysterious) sequence of numbers. The point is just to have a not
completely trivial program to discuss and outfit with testing.
-----------------------------------------------------------------------
<file "0/main.cpp">
#include "io_util.hpp"
using my::int_from_user;
using namespace std;
auto main() -> int
{
int const ddx = int_from_user( "Delta? " );
int const n = int_from_user( "Number of numbers? " );
int x = 0;
int dx = 0;
for( int count = 1; count <= n; ++count )
{
dx += ddx; x += dx;
if( count > 1 ) { cout << ", "; }
cout << x;
}
cout << endl;
}
</file>
*** STEP 1
We choose the worst possible ways to add some unit testing to code,
namely, we'll test generated text rather than the data itself, and we'll
do this by sprinkling preprocessor directives all over the code in order
to have just one source code file that's everything and that can be
built as either the original program, or as a tester program:
-----------------------------------------------------------------------
<file "1/main.cpp">
#include "io_util.hpp"
using my::int_from_user;
#include <assert.h> // assert
#include <iostream>
#include <stdexcept> // std::runtime_error
#include <stdlib.h> // EXIT_FAILURE, EXIT_SUCCESS
#include <string> // std::string
#include <sstream> // std::ostringstream;
#include <utility> // std::begin, std::end
using namespace std;
#define IS_UNUSED( arg ) (void) arg; struct arg
auto main( int n_args, char* args[] ) -> int
{
#ifdef TEST
assert( n_args == 2 );
int const ddx = stoi( args[1] );
int const n = 7;
ostringstream output;
#else
IS_UNUSED( n_args ); IS_UNUSED( args );
int const ddx = int_from_user( "Delta? " );
int const n = int_from_user( "Number of numbers? " );
ostream& output = cout;
#endif
int x = 0;
int dx = 0;
for( int count = 0; count < n; ++count )
{
dx += ddx; x += dx;
if( count > 0 ) { output << ", "; }
output << x;
}
output << endl;
#ifdef TEST
string const expecteds[] =
{
"1, 3, 6, 10, 15, 21, 28\n",
"2, 6, 12, 20, 30, 42, 56\n",
"3, 9, 18, 30, 45, 63, 84\n"
};
assert( ddx <= end( expecteds ) - begin( expecteds ) );
string const& expected = expecteds[ddx - 1];
bool const ok = (output.str() == expected);
cout << (ok? "OK" : "Failed") << " (ddx = " << ddx << ")" << endl;
if( not ok )
{
cout << "Expected '" << expected << "' but got '" <<
output.str() << "'" << endl;
}
return (ok? EXIT_SUCCESS : EXIT_FAILURE);
#endif
}
</file>
One main problem with this is that it looks complex.
That's a problem because maintenance is about 80% of all programming,
and apparent complexity makes the eyes of the maintainer glaze over.
Getting down to nitty-gritty concrete problems, then, for example, any
change of the output format requires updating the test expectations. The
test code even depends on the internal variable names in the code to be
tested. And not just that, but the main code's stream reference called
"output" is there solely in order to support passing results to the
testing code.
*** STEP 2
Instead of conditional compilation and one single file, the function to
be tested should be put in a module of its own, either separately
compiled or as a header only module. Then a main program that doesn't
test, is easily implemented. And likewise, a program that only tests, is
also easily implemented, independently of the first one.
Since the code generates a sequence of numbers a natural choice is to
factor it out as a function that returns a "vector<int>".
It's not the most general choice available, but it's simple and
convenient until one needs something less memory-hungry:
-----------------------------------------------------------------------
<file "2/number_generator.hpp">
#pragma once
#include <vector>
namespace my {
using std::vector;
inline
auto generate_numbers( int const delta, int const n )
-> vector<int>
{
vector<int> result;
int x = 0;
int dx = 0;
for( int i = 1; i <= n; ++i )
{
dx += delta;
x += dx;
result.push_back( x );
}
return result;
}
} // namespace my
</file>
Now the ordinary main program can be pretty simple:
-----------------------------------------------------------------------
<file "2/main.cpp">
#include "number_generation.hpp"
#include "io_util.hpp"
using my::Output_value_list;
auto main() -> int
{
using my::int_from_user;
using my::generate_numbers;
int const ddx = int_from_user( "Delta? " );
int const n = int_from_user( "Number of numbers? " );
Output_value_list output( cout );
for( int const x : generate_numbers( ddx, n ) )
{
output( x );
}
cout << endl;
}
</file>
The testing program is only slightly less simple:
-----------------------------------------------------------------------
<file "2/test.cpp">
#include "number_generation.hpp"
#include "io_util.hpp"
using my::Output_value_list;
#include <assert.h> // assert
#include <map> // std::map
#include <stdlib.h> // EXIT_FAILURE, EXIT_SUCCESS
using namespace std;
auto operator<<( ostream& stream, vector<int> const& v )
-> ostream&
{
Output_value_list output( stream );
for( int const x : v ) { output( x ); }
return stream;
}
auto main( int n_args, char* args[] ) -> int
{
using my::generate_numbers;
assert( n_args == 2 );
int const ddx = stoi( args[1] );
int const n = 7;
vector<int> const numbers = generate_numbers( ddx, n );
map< int, vector<int> > const expecteds =
{
{1, {1, 3, 6, 10, 15, 21, 28}},
{2, {2, 6, 12, 20, 30, 42, 56}},
{3, {3, 9, 18, 30, 45, 63, 84}}
};
bool const ok = (numbers ==
expecteds.at( ddx ));
cout << (ok? "OK" : "Failed") << " (ddx = " << ddx << ")" << endl;
if( not ok )
{
cout << "Expected '" <<
expecteds.at( ddx ) << "' but got '" <<
numbers << "'" << endl;
}
return (ok? EXIT_SUCCESS : EXIT_FAILURE);
}
</file>
If the code is changed so that arbitrary long sequences need to be
tested, then one way to deal with that is that instead of returning a
"vector<int>", the basic function just calls a supplied function with
each position and value, as they're encountered. But this posting is
already long enough, so I choose to not show that here. Just a final
note: I have intentionally refrained from generalizing things, like, the
operator<< here is not very general, and that's IMHO as it should be for
example code -- just very concrete.
Also, it can be a good idea to use Someone Else(TM)'s unit testing
framework. E.g. the Google one.
Cheers & hth.,
- Alf
--
Using Thunderbird as Usenet client, Eternal September as NNTP server.