- Author : Christopher Batten
- Date : September 24, 2008
The unit test subproject provides a simple way to write and manage unit tests. Although we could write unit tests with ad-hoc test code, there are several advantages to using some kind of unit testing framework. A common unit test framework simplifies writing tests, makes it easier for developers to understand each other's tests, provides a consistent way to run and log tests, and reduces false errors due to incorrect testing code. It is important to make unit testing consistent and simple since a good developer will spend almost as much (if not more) time writing unit tests as they do writing the actual code being tested.
A test includes a set of checks which are grouped into test cases which are then further grouped into test suites. Running the tests is controlled by a test driver. Much of the details of the framework are abstracted away behind preprocessor macros and helper functions, making it quite simple to write basic unit tests.
This user guide is divided into two parts: the first part is a tutorial,
and the second part contains general user reference. We will assume that
you are using the unit testing framework as part of the Modular C++
Build System so you might want to read the documentation for the build
system first mcppbs-uguide.md
.
-
Part 1 : Tutorial
- 1.1. Basic Unit Test for the STL Vector Class
- 1.2. Additional Test Cases for the STL Vector Class
- 1.3. Using Parameterized Test Cases
- 1.4. Using the Extra Test Suites
- 1.5. Controlling Which Test Cases Are Run
-
Part 2 : Reference
- 2.1. The Basic
UTST_CHECK
Macros - 2.2. Grouping Checks into Test Cases
- 2.3. Collecting Test Cases into Test Suites
- 2.4. Managing Test Suites with Test Drivers
- 2.5. Using the Auto Registration Features
- 2.6. Integrating Unit Tests into the Build System
- 2.1. The Basic
- Tutorial
This tutorial walks through using the unit testing framework to test the
standard STL vector
class. You can follow along through the tutorial
yourself by typing in the commands marked with a %
symbol at the shell
prompt. Tasks to try on your own are denoted with (*). To cut and paste
commands from this tutorial into your bash shell (and make sure bash
ignores the %
character) just use an alias to "undefine" the %
character like this:
% alias %=""
Before working through the tutorial we need to instantiate the Modular
C++ Build System which contains the utst subproject. We do this by first
creating a temporary directory to work in, setting an environment
variable to simplify the rest of the tutorial, and downloading the
mcppbs
project from the public git repository.
% TGZ=<location-of-template>
% wget -O- $TGZ | tar xzf -
% mv cbatten-sw-mcppbs mcppbs-tut
% cd mcppbs-tut
% PROJROOT=$PWD
Now that we have the build system let's go ahead and build the project, although the only subproject in the default project is the unit test framework itself.
% cd $PROJROOT
% mkdir build
% cd build
% ../configure
% make
To run all of the unit tests use use the makefile's check
target.
% cd $PROJROOT/build
% make check
As explained in the user guide for the build system, the way we add code
to our project is through subprojects. We assume you are familiar with
how the build system works, so we will just quickly create a subproject
for this tutorial called utst-tut
.
% cd $PROJROOT
% mkdir utst-tut
% echo "MCPPBS_SUBPROJECT([utst-tut])" > utst-tut/utst-tut.ac
% cat > utst-tut/utst-tut.mk.in \
<<'END'
utst_tut_intdeps = @utst_tut_intdeps@
utst_tut_cppflags = @utst_tut_cppflags@
utst_tut_ldflags = @utst_tut_ldflags@
utst_tut_libs = @utst_tut_libs@
END
% cd $PROJROOT
% sed -i.bak '/MCPPBS_INCLUDE_INTERNAL(\[utst\])/ a \
MCPPBS_INCLUDE_INTERNAL([utst-tut])
' configure.ac
% autoconf && autoheader
Normally, we each physical "component" (i.e. a C++ header and source
pair) has an associated unit test with a .t.cc
suffix, but for this
tutorial we will only write the unit tests.
Usually, every physical component (ie. a header/source file pair) should
have an additional unit test file with the special .t.cc
extension.
For example, let's assume I am working on a new class named Bar
in the
foo
subproject. The declaration of Bar
will be in foo/Bar.h
and
the definition will be in foo/Bar.cc
. My unit tests should be placed
in foo/Bar.t.cc
. As set by the general build policies, these three
files will be in the foo
subdirectory. To tell the build system about
these new files, the developer needs to add them to specific make
variables in foo/foo.mk.in
: foo/Bar.h
is added to foo_hdrs
,
foo/Bar.cc
is added to foo_srcs
, and foo/Bar.t.cc
is added to
foo_test_srcs
. The build system will automatically include rules to
build a foo/Bar-utst
executable from foo/Bar.t.cc
. However, for this
tutorial we will simplify things and only write the unit tests (not the
header/source files for the actual things we might be testing).
The most straightforward way to define a test case is to use the
UTST_AUTO_TEST_CASE
macro. We will learn later in the reference
sections exactly what this macro does, but for now all we need to know
is that we give the macro the name of the test and use braces to define
the body of the test. The test name should be camel case and often we
begin the name with Test
. The following test case uses this macro to
perform some basic tests of the STL vector
class.
% cd $PROJROOT
% cat > utst-tut/Test1.t.cc \
<<'END'
#include "utst/utst.h"
#include <vector>
UTST_AUTO_TEST_CASE( TestBasic )
{
std::vector<int> vec;
UTST_CHECK( vec.empty() );
UTST_CHECK_EQ( vec.size(), 0u );
vec.resize(3);
vec.at(0) = 0;
vec.at(1) = 1;
vec.at(2) = 2;
UTST_CHECK( !vec.empty() );
UTST_CHECK_EQ( vec.size(), 3u );
UTST_CHECK_EQ( vec.at(0), 0 );
UTST_CHECK_EQ( vec.at(1), 1 );
UTST_CHECK_EQ( vec.at(2), 2 );
}
int main( int argc, char* argv[] )
{
utst::auto_command_line_driver( argc, argv, "utst-tut" );
}
END
Notice that we need to include the utst.h
header and we use the
UTST_CHECK
and UTST_CHECK_EQ
macros to perform various tests. The
rest of the UTST_CHECK
macros are described in the reference sections.
Our main
function is relatively simple - we just call the built-in
command line driver which will parse the command line and execute the
tests we have listed in this file. As a quick aside, we are using the
automatic registration features of the framework in this unit test. The
framework automatically registers the TestBasic
test case with a
default global test suite, and the command line driver automatically
references this global test suite. We will learn more in later sections
about how to write test cases which need to be explicitly registered
with a test suite, and also about the various global test suites.
Now that we have a unit test we need to add it to the list of unit tests in our subproject's make fragment.
% cd $PROJROOT
% echo "utst_tut_test_srcs += utst-tut/Test1.t.cc" >> utst-tut/utst-tut.mk.in
We are finally ready to build and run our first test case using the build system.
% cd $PROJROOT/build
% make utst-tut/Test1-utst
% ./utst-tut/Test1-utst
The output from running the unit test should be:
Unit Tests : utst-tut/Test1
Running test suite : default
+ Running test case : TestBasic
Essentially the build system knows how to compile all the .t.cc
test
source files into executables - one binary per unit test. Each binary is
named the same as the basename of the .t.cc
source with an additional
-utst
suffix. The output from running the unit test shows that we have
run the default test suite for the utst-tut/Test1
unit test. Use
--help
to see the various command line options provided by the unit
test framework. For example, to see more output use the --log-level
option.
% ./utst-tut/Test1-utst --log-level verbose
The output from running the unit test will now be more verbose:
Unit Tests : utst-tut/Test1
Running test suite : default
+ Running test case : TestBasic
[ passed ] Line 9 : vec.empty()
[ passed ] Line 10 : vec.size() == 0u [ 0 == 0 ]
[ passed ] Line 17 : !vec.empty()
[ passed ] Line 18 : vec.size() == 3u [ 3 == 3 ]
[ passed ] Line 19 : vec.at(0) == 0 [ 0 == 0 ]
[ passed ] Line 20 : vec.at(1) == 1 [ 1 == 1 ]
[ passed ] Line 21 : vec.at(2) == 2 [ 2 == 2 ]
The extra information shows the result of each UTST_CHECK
regardless
of whether or not it passed or failed. We are often repeatedly building
and running a unit test in our edit-compile-debug loop. You can combine
the building and execution of a unit test into a single command line
with the &&
shell operator. The &&
operator means that we will only
try and run the unit test if in fact we were able to successfully build
the unit test.
% make utst-tut/Test1-utst && ./utst-tut/Test1-utst
() Try purposely changing the TestBasic
test cases so that it will
fail. For example, change the test for the size of the vector from 3 to
2. Then rerun the tests and notice how the failure is reported. () Try
all three log levels and see how the reporting for passing and failing
test cases changes.
In this section we will experiment with some additional test cases. Normally we might just append these to the end the previous unit test, for the purposes of this tutorial we will just make a completely new unit test source file.
% cd $PROJROOT
% cat > utst-tut/Test2.t.cc \
<<'END'
#include "utst/utst.h"
#include <vector>
#include <list>
#include <stdexcept>
UTST_AUTO_TEST_CASE( TestRangeException )
{
std::vector<int> vec(3);
vec.at(0) = 0;
vec.at(1) = 1;
vec.at(2) = 2;
UTST_CHECK_THROW( std::out_of_range, vec.at(3) );
}
UTST_AUTO_TEST_CASE( TestPushBack )
{
std::list<int> lst;
lst.push_back(0);
lst.push_back(1);
lst.push_back(2);
std::vector<int> vec;
vec.push_back(0);
vec.push_back(1);
vec.push_back(2);
UTST_CHECK_CONT_EQ( lst, vec );
}
int main( int argc, char* argv[] )
{
utst::auto_command_line_driver( argc, argv, "utst-tut" );
}
END
The UTST_CHECK_THROW
macro checks if an expression throws a specific
exception. In the above example, we use this macro to verify that the
vector
class throws a std::out_of_range
exception when we access an
element which is beyond the size of the vector. The UTST_CHECK_CONT_EQ
macro verifies that two containers are the same. It requires that the
containers have begin()
, end()
, and size()
methods. Let's add the
new unit test to the make fragment and compile and built the new unit
test.
% cd $PROJROOT
% echo "utst_tut_test_srcs += utst-tut/Test2.t.cc" >> utst-tut/utst-tut.mk.in
% cd $PROJROOT/build
% make utst-tut/Test2-utst && ./utst-tut/Test2-utst
(*) Try adding a new test case to the utst-tut/Test2 unit test which
tests the insert
and erase
methods.
Although most of the time we will use the UTST_AUTO_TEST_CASE
macro
sometimes we may want to use the classes in the unit test framework
explicitly. For example, we might want a test case which takes a few
constructor arguments that control what we are checking. Such
parameterized test cases allow us to run many different tests just by
calling the same test case with different arguments. To implement such
test cases we explicitly subclass the ITestCase_BasicImpl
base class
which is described in more detail in the reference sections.
The following example tests the push_back
method but it is
parameterized by how many elements to actually push into the vector. To
test push back we first assume that the STL list
container has already
been unit tested. So we randomly pick a number, push it into both the
vector and the list, and then check at the end that both containers have
the same values.
% cd $PROJROOT
% cat > utst-tut/Test3.t.cc \
<<'END'
#include "utst/utst.h"
#include <vector>
#include <list>
class TestPushBackParam
: public utst::ITestCase_BasicImpl<TestPushBackParam>
{
public:
TestPushBackParam( int size ) : m_size(size)
{
std::ostringstream ost;
ost << "TestPushBack_size" << size;
set_name( ost.str() );
}
void the_test()
{
std::list<int> lst;
std::vector<int> vec;
for ( int i = 0; i < m_size; i++ ) {
int random_int = rand() % 100;
lst.push_back( random_int );
vec.push_back( random_int );
}
UTST_CHECK_CONT_EQ( lst, vec );
}
private:
int m_size;
};
using namespace utst;
AutoRegister ar_test1 ( &g_default_suite(), TestPushBackParam(1) );
AutoRegister ar_test2 ( &g_default_suite(), TestPushBackParam(2) );
AutoRegister ar_test100 ( &g_default_suite(), TestPushBackParam(100) );
int main( int argc, char* argv[] )
{
utst::auto_command_line_driver( argc, argv, "utst-tut" );
}
END
Although we are explicitly instantiating a test case we can still
leverage the auto registration features through the AutoRegister
class. This class essentially registers the given test case in the given
test suite when it is instantiated. Again, these auto registration
features prevent us from having to explicitly register tests cases in
the main function. The reference section explains how to do the
registration manually although it is rarely necessary. Essentially the
UTST_AUTO_TEST_CASE
macro takes care of defining a new test case class
which inherits from ITestCase_BasicImpl
and then instantiating an
appropriate AutoRegister
object.
We instantiate three test cases which will test pushing one, two, and 100 elements. We now add the new unit test to the make fragment, build, and run the new test cases.
% cd $PROJROOT
% echo "utst_tut_test_srcs += utst-tut/Test3.t.cc" >> utst-tut/utst-tut.mk.in
% cd $PROJROOT/build
% make utst-tut/Test3-utst && ./utst-tut/Test3-utst
There are a total of three global test suites:
g_default_suite()
: For default functional unit testsg_longrun_suite()
: For long running functional unit testsg_perf_suite()
: For performance regression tests
We have already seen that the UTST_AUTO_TEST_CASE
defines new test
cases and automatically adds them to the default test suite. For the
other two test suites we can use the UTST_AUTO_EXTRA_TEST_CASE
macro
as follows:
UTST_AUTO_EXTRA_TEST_CASE( suite_name, TestCase )
For the suite name we should use either longrun
or perf
. For
example, the following test cases measure how long it takes to
initialize a thousand element vector using the assign statement versus
the push_back
method.
% cd $PROJROOT
% cat > utst-tut/Test4.t.cc \
<<'END'
#include "utst/utst.h"
#include <vector>
UTST_AUTO_EXTRA_TEST_CASE( perf, PerfAssign )
{
using namespace std;
int num_trials = 1e6;
int num_elm_per_vec = 1e3;
clock_t start = clock();
for ( int i = 0; i < num_trials; i++ ) {
std::vector<int> vec(num_elm_per_vec);
for ( int j = 0; j < num_elm_per_vec; j++ )
vec[j] = j;
}
clock_t end = clock();
utst::TestLog& log = utst::TestLog::instance();
ostream& os = log.get_log_ostream( utst::TestLog::LogLevel::minimal );
os << " - assign_num_trials = " << num_trials << endl;
os << " - assign_num_elm = " << num_elm_per_vec << endl;
os << " - assign_total_time = " << (end - start) << endl;
}
UTST_AUTO_EXTRA_TEST_CASE( perf, PerfPushBack )
{
using namespace std;
int num_trials = 5e5;
int num_elm_per_vec = 1e3;
clock_t start = clock();
for ( int i = 0; i < num_trials; i++ ) {
std::vector<int> vec;
for ( int j = 0; j < num_elm_per_vec; j++ )
vec.push_back(j);;
}
clock_t end = clock();
utst::TestLog& log = utst::TestLog::instance();
ostream& os = log.get_log_ostream( utst::TestLog::LogLevel::minimal );
os << " - push_num_trials = " << num_trials << endl;
os << " - push_num_elm = " << num_elm_per_vec << endl;
os << " - push_total_time = " << (end - start) << endl;
}
int main( int argc, char* argv[] )
{
utst::auto_command_line_driver( argc, argv, "utst-tut" );
}
END
Notice how these test cases use the test log. By passing
LogLevel::minimal
to the get_log_ostream
method, the test case is
telling the test log that whatever is inserted into the log stream
should be displayed whenever the log level is equal to or more verbose
than minimal
(ie. always). So the performance results will always be
displayed regardless of log level. The following test case measures how
long it takes to initialize a thousand element vector using the
push_back
method.
% cd $PROJROOT
% echo "utst_tut_test_srcs += utst-tut/Test4.t.cc" >> utst-tut/utst-tut.mk.in
% cd $PROJROOT/build
% make utst-tut/Test4-utst && ./utst-tut/Test4-utst
The output from these performance tests should look something like:
Unit Tests : utst-tut-Test4
Running test suite : perf
+ Running test case : PerfAssign
- assign_num_trials = 1000000
- assign_num_elm = 1000
- assign_total_time = 197
+ Running test case : PerfPushBack
- push_num_trials = 500000
- push_num_elm = 1000
- push_total_time = 374
These results show that using the assignment statement (which allocates
all of the memory first) is faster than using repeated calls to the
push_back
(which logarithmically increases the allocated space). (*)
Try adding a new test case to the longrun
test suite.
As one adds more and more test cases to their unit test they may want to
run manage which test cases are run in a more fine-grain way. You can
use the --list-tests
command line option to see which test cases are
available in which test suites. To run a specific test case or test
suite simply specify its name on the command line. To just run the
TestRangeException
test case or to just run the perf
test suite from
some of the earlier examples we would use the following:
% ./utst-tut/Test2-utst TestRangeException
% ./utst-tut/Test4-utst perf
We can run all the unit tests for this subproject with the
check-utst-tut
make target and as mentioned earlier we can run all the
unit tests for all test cases with the check
make target.
% make check
What the check
target does is build and run each unit test in the
default test suite and save the output from each unit test in a .out
file. Then it greps through these .out
files for errors to present a
very compact summary of the unit test results.
- Reference
In this part, we walk through how to use the unit test framework starting with the simplest possible way to write unit tests and then gradually discussing more features of the framework.
Let's assume we want to add some unit tests for some functions or a
class we have written. The most basic way to do this is to create a
separate toplevel source file and write some tests for our class in the
toplevel source file's main
function. We can use the UTST_CHECK
macros located in utst/Checks.h
to check for various conditions which
we know should be true. The basic UTST_CHECK
macros are listed below.
UTST_CHECK( e1 )
: Does expression e1 eval true?UTST_CHECK_EQ( e1, e2 )
: Are two expressions equal?UTST_CHECK_NEQ( e1, e2 )
: Are two expressions not equal?UTST_CHECK_CONT_EQ( c1, c2 )
: Are two containers equal?UTST_CHECK_THROW( excep, e1 )
: Expression throws given exception?UTST_CHECK_FAILED( msg )
: Always fails with given message
Unlike UTST_CHECK( e1 == e2 )
, using the UTST_CHECK_EQ( e1, e2 )
macro will display the values of both expressions which is useful for
debugging. Similarly the UTST_CHECK_NEQ
and UTST_CHECK_CONT_EQ
macros can be more useful that just using UTST_CHECK
directly. The
UTST_CHECK_THROW
macro will see if the given expression throws the
given exception and then continue executing. The UTST_CHECK_FAILED
macro is useful when the condition for failure is too complicated for
the basic macros (eg. we want to check if a data structure contains some
value and if not the check should fail with a meaningful message).
Each macro does the appropriate check and then logs its result to the
global TestLog
. The TestLog
manages the output for the unit test
framework; a developer can control how much output is displayed by
setting the log level to one of three values:
minimal
: Output failing checks onlymoderate
: Output failing checks and other log outputverbose
: Output passing and failing checks and other log output
For example, a developer can set the log level to moderate as follows:
utst::TestLog& log = utst::TestLog::instance();
log.set_log_level( utst::TestLog::LogLevel::moderate );
Notice that there is only one global test log and it is accessed through
the static singleton method TestLog::instance()
. Also notice that the
log level names are static constants in the TestLog::LogLevel
namespace. A developer can write general information to the test log by
inserting into the output stream provided through the
TestLog::get_log_ostream
method. Here is an example:
std::ostream& os = utst::TestLog::instance().get_log_ostream();
os << "Starting new set of checks ..." << std::endl;
The get_log_ostream
method takes an optional parameter which sets the
the minimum log level at which whatever is inserted into the stream
should be displayed. For example, if we pass LogLevel::verbose
into
get_log_ostream
, then whatever is inserted into that stream will only
be displayed if the log level is set to verbose. The default is
moderate
meaning the output is only displayed if the log level is set
to moderate
or verbose
.
In the following code we use the UTST_CHECK
macros to write a simple
unit test for the standard STL vector
class.
int main( int argc, char* argv[] )
{
utst::TestLog& log = utst::TestLog::instance();
log.set_log_level( utst::TestLog::LogLevel::verbose );
std::vector<int> vec(2);
vec.at(0) = 0; vec.at(1) = 1;
UTST_CHECK_EQ( vec.size(), 2u );
UTST_CHECK_EQ( vec.at(0), 0 );
UTST_CHECK_EQ( vec.at(1), 1 );
}
Obviously we can add even more checks, possibly using the
UTST_CHECK_THROW
macro to make sure the vector
class throws a range
exception for out-of-bounds accesses or using the UTST_CHECK_CONT_EQ
macro to compare different instances of the vector
class.
We can use test cases to better structure the checks used in our unit tests. A test case is simply a named group of checks which test a specific functionality of interest.
Test cases are implemented as classes which derive from the common
abstract base class ITestCase
. Instead of deriving from ITestCase
directly (and thus having to implement all of the virtual functions from
scratch), developers can derive from the implementation helper class
ITestCase_BasicImpl
. ITestCase_BasicImpl
is a subclass of
ITestCase
which implements all of the virtual functions except for a
new protected member called the_test
. To create a new test case,
developers just derive from ITestCase_BasicImpl
and overload
the_test
with the appropriate checks. The reason developers overload
ITestCase_BasicImpl::the_test
instead of ITestCase::run
directly, is
because the implementation helper class adds some code before and after
it executes the_test
. It writes some log output indicating that it is
starting a test case and it also catches any exceptions thrown from
inside the_test
. If code within the_test
throws an exception which
is not caught then the test case fails but the program keeps executing.
As an example of using ITestCase_BasicImpl
, the following test case
encapsulates the basic tests from the previous section.
class TestBasic : public utst::ITestCase_BasicImpl<TestBasic> {
public:
TestBasic() { set_name( "TestBasic" ); }
void the_test()
{
std::vector<int> vec(2);
vec.at(0) = 0; vec.at(1) = 1;
UTST_CHECK_EQ( vec.size(), 2u );
UTST_CHECK_EQ( vec.at(0), 0 );
UTST_CHECK_EQ( vec.at(1), 1 );
}
};
ITestCase_BasicImpl
is templated by the type of the derived class.
This is so the base class can implement the clone
method. Notice that
in addition to implementing the_test
, a developer also needs to add a
constructor which sets the name of the test case. The policy is that the
name of the test case should be the same as the name of the class which
implements that test case.
The framework provides a macro called UTST_TEST_CASE
which makes it
much easier to write these type of basic test cases. The following
example shows how to use this macro to write the same basic test case.
UTST_TEST_CASE( TestBasic )
{
std::vector<int> vec(2);
vec.at(0) = 0; vec.at(1) = 1;
UTST_CHECK_EQ( vec.size(), 2u );
UTST_CHECK_EQ( vec.at(0), 0 );
UTST_CHECK_EQ( vec.at(1), 1 );
}
This macro will create a new class called TestBasic
and it adds the
code in the associated { }
block to the_test
. Even though this macro
makes it very easy to write test cases, there are still times where we
want to explicitly subclass ITestCase_BasicImpl
. For example, we might
want a test case which takes a few constructor arguments that control
what we are checking. These parameterized test cases allow us to run
many different tests just by calling the same test case with different
arguments.
The following example is similar to before, except that it is
parameterized by the size of the vector. For this test we first assume
that the STL list
container has already been unit tested. So we
randomly pick a number, add it into both the vector
and the list
,
and then check at the end that both containers have the same values.
class TestBasic : public utst::ITestCase_BasicImpl<TestBasic> {
public:
TestBasic( int size ) : m_size(size)
{
std::ostringstream ost;
ost << "TestBasic_size" << size;
set_name( ost.str() );
}
void the_test()
{
std::list<int> lst;
std::vector<int> vec;
for ( int i = 0; i < m_size; i++ ) {
int random_int = rand() % 100;
lst.push_back( random_int );
vec.push_back( random_int );
}
UTST_CHECK_CONT_EQ( lst, vec );
}
private:
int m_size;
};
Notice that we create the test case name from the arguments so that the various instantiations of this test case have different and descriptive names.
So let's assume we have created several different test cases which each
check different aspects of the standard STL vector
class. We can then
instantiate these test cases and run them in main
. Since test cases
are just normal objects, they can be stored in a container just like any
other object with a common base class. Then we can iterate over the
container and call the run
virtual function. Notice how we use the
parameterized test case to test the push back with one and 100 elements.
int main( int argc, char* argv[] )
{
utst::TestLog& log = utst::TestLog::instance();
log.set_log_level( utst::TestLog::LogLevel::verbose );
// Instantiate the test cases and add them to a container
std::list<utst::ITestCase*> tests;
tests.push_back( new TestBasic() );
tests.push_back( new TestRangeException() );
tests.push_back( new TestPushBack(1) );
tests.push_back( new TestPushBack(100) );
// Iterate over test cases and run them
typedef std::list<utst::ITestCase*>::iterator ITR;
for ( ITR itr = tests.begin(); itr != tests.end(); ++itr )
(*itr)->run();
// Delete the test cases since they were dynamically allocated
for ( ITR itr = tests.begin(); itr != tests.end(); ++itr )
delete *itr;
}
To simplify managing collections of test cases we can group them into test suites. Organizing unit tests into test cases and test suites allows us to easily just run a single test case or run all the tests in a suite. Developers might create multiple test suites for different types of testing. For example, we might have a policy that in addition to the default test suite, developers can create extra suites with long-running test cases and/or performance tests. In the following code, we add the test cases from the previous section to a new test suite and then run all the tests.
int main( int argc, char* argv[] )
{
utst::TestLog& log = utst::TestLog::instance();
log.set_log_level( utst::TestLog::LogLevel::verbose );
// Instantiate the test cases and add them to a test suite
utst::TestSuite suite("std::vector");
suite.add_test( TestBasic() );
suite.add_test( TestRangeException() );
suite.add_test( TestPushBack(1) );
suite.add_test( TestPushBack(100) );
// Run all the test cases
suite.run_all();
}
Test drivers manage setting up the test log and running the desired
tests. Currently there is only one type of test driver
(CommandLineTestDriver
) which uses the command line to control the
unit testing. One can imagine additional test drivers which might use an
XML file or a GUI to control the unit testing. New test drivers might
also demand different forms of test output, and thus we might need to
improve how the utst::TestLog
is handled. For now, we focus on
providing a clean way to manage unit tests from the command line.
Test drivers enable a developer to work with several test suites at the same time. In addition to a basic default test suite for functional unit testing, we might want to add a test suite for performance regression tests. The following code illustrates how to use the command line test driver.
int main( int argc, char* argv[] )
{
// Instantiate functional test cases and add them to default suite
utst::TestSuite default_suite("default");
default_suite.add_test( TestBasic() );
default_suite.add_test( TestRangeException() );
default_suite.add_test( TestPushBack(1) );
default_suite.add_test( TestPushBack(100) );
// Instantiate performance test cases and add them to perf suite
utst::TestSuite perf_suite("perf");
perf_suite.add_test( PerfAssign() );
perf_suite.add_test( PerfPushBack() );
// Use command line to drive the tests
utst::CommandLineTestDriver driver;
driver.add_suite( &default_suite );
driver.add_suite( &perf_suite );
driver.run( argc, argv );
}
The test driver accepts the following command line usage:
> utst-exe [options] [suite ...] [case ...]
With the following command line options:
--log-level <level>
: Set level of detail for output--list-tests
: List all test suites and cases--help
: Show usage information
Use the --list-tests
flag to see a list of all the possible test
suites and test cases. By default all test suites are run, but often we
only want to run a subset of the tests. For example, we might want to
run a single test case to debug it quickly, or we might want to disable
some long running performance tests. A user can select which tests to
run by simply listing the names of the desired test suites and test
cases on the command line. Possible values for the log levels are:
minimal
, moderate
, and verbose
.
Note that the test driver will have trouble if you use the same name for multiple test cases or test suites. Try to use unique lower case names for test suites and unique camel case names for the test cases.
A common error when using test cases and test suites is to define a new
test case but forget to add it to the appropriate test suite. The auto
registration features can help avoid these kind of errors. The
utst/AutoUtils.h
header declares global test suites as well as helper
classes and macros for automatically adding test cases to these global
test suites. The three global test suites are as follows:
g_default_suite()
: For default functional unit testsg_longrun_suite()
: For long running functional unit testsg_perf_suite()
: For performance regression tests
To avoid issues with static initialization order, these global test
suites are not implemented with global variables. Instead they are
implemented as free functions which return a reference to a local static
variable. Here is an example of defining a new test case and then using
the AutoRegister
class to register the test case right away.
class TestBasic : public utst::ITestCase_BasicImpl<TestBasic> {
public:
TestBasic() { set_name( "TestBasic" ); }
void the_test()
{
std::vector<int> vec(2);
vec.at(0) = 0; vec.at(1) = 1;
UTST_CHECK_EQ( vec.size(), 2u );
UTST_CHECK_EQ( vec.at(0), 0 );
UTST_CHECK_EQ( vec.at(1), 1 );
}
};
utst::AutoRegister
ar_TestBasic( &utst::g_default_suite(), TestBasic() );
The key idea is that we want to register this test case with the global
default test suite right now and not wait until the main
function. We
can't really execute any code right away, though, since we are just at
the general file scope. We can, however, execute some code in a
constructor for a class which we instantiate. The AutoRegister
class
is just a simple helper whose constructor takes two arguments: a test
suite and a test case. The constructor just adds the test case to the
test suite. If we instantiate a new AutoRegister
object right away
then we can uses its constructor to execute the desired code.
Of course this isn't really easier than remembering to add the test case
to the test suite in main
. The UTST_AUTO_TEST_CASE
macro does all
this for us and makes it trivial to define new test cases which are
automatically added to the global default test suite.
UTST_AUTO_TEST_CASE( TestBasic )
{
std::vector<int> vec(2);
vec.at(0) = 0; vec.at(1) = 1;
UTST_CHECK_EQ( vec.size(), 2u );
UTST_CHECK_EQ( vec.at(0), 0 );
UTST_CHECK_EQ( vec.at(1), 1 );
}
To add test cases to one of the other (non-default) test suites, we use
the UTST_AUTO_EXTRA_TEST_CASE
macro which takes an additional argument
specifying the desired global test suite.
UTST_AUTO_EXTRA_TEST_CASE( longrun, TestBasic )
{
std::vector<int> vec(2);
vec.at(0) = 0; vec.at(1) = 1;
UTST_CHECK_EQ( vec.size(), 2u );
UTST_CHECK_EQ( vec.at(0), 0 );
UTST_CHECK_EQ( vec.at(1), 1 );
}
Finally, in the unit test's main
function we can use the
auto_command_line_driver
free function to automatically instantiate a
CommandLineTestDriver
, add the three global test suites, and run the
unit tests. Then our main
function simply looks like this:
void main( int argc, char* argv[] )
{
utst::auto_command_line_driver( argc, argv );
}
The auto registration features make writing unit tests very simple. Most
of the time we write our test cases using UTST_AUTO_TEST_CASE
and
sometimes we use UTST_AUTO_EXTRA_TEST_CASE
when we want to add a test
case to either the longrun
or the perf
test suites. In the unit
test's main
function we just call the auto_command_line_driver
free
function.
Although the auto registration features makes unit testing simple, we can always explicitly use the framework as described in the earlier sections when we need to do something non-standard (eg. parameterized test cases, additional test suites, etc).