...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
It is possible to pass custom command line arguments to the test module. The general format for passing custom arguments is the following:
<boost_test_module> [<boost_test_arg1>...] [-- [<custom_parameter1>...]
This means that everything that is passed after "--
"
is considered as a custom parameter and will not be intercepted nor interpreted
by the Unit Test Framework. This avoids any troubleshooting
between the Unit Test Framework parameters and the custom
ones.
There are several use cases for accessing the arguments passed on the command line:
In the first scenario, test cases or fixtures, including global fixtures, may be used. Since those are part of the test tree, they can benefit from the Unit Test Framework rich set of assertions and controlled execution environment.
In the second scenario, the command line argument interact directly with the content of the test tree: by passing specific arguments, different set of tests are created. There are mainly two options for achieving this: using a dedicated initialization function or using data driven test cases. The error handling of the command line parameters needs however to be adapted.
The master test suite collects the custom arguments passed to the test module in the following way:
argv[0]
, usually
set by the operating system as the executable name, remains unchanged
argv
--
is removed
as well
argv
starting at index
1
Code |
---|
#define BOOST_TEST_MODULE runtime_configuration1 #include <boost/test/included/unit_test.hpp> using namespace boost::unit_test; BOOST_AUTO_TEST_CASE(test_accessing_command_line) { BOOST_TEST_REQUIRE( framework::master_test_suite().argc == 3 ); BOOST_TEST( framework::master_test_suite().argv[1] == "--specific-param" ); BOOST_TEST( framework::master_test_suite().argv[2] == "'additional value with quotes'" ); BOOST_TEST_MESSAGE( "'argv[0]' contains " << framework::master_test_suite().argv[0] ); } |
Output |
---|
> runtime_configuration1 --log_level=all --no_color -- --specific-param "'additional value with quotes'" Running 1 test case... Entering test module "runtime_configuration" test.cpp:14: Entering test case "test_accessing_command_line" test.cpp:16: info: check framework::master_test_suite().argc == 3 has passed test.cpp:17: info: check framework::master_test_suite().argv[1] == "--specific-param" has passed test.cpp:18: info: check framework::master_test_suite().argv[2] == "'additional value with quotes'" has passed 'argv[0]' contains runtime_configuration1 test.cpp:14: Leaving test case "test_accessing_command_line"; testing time: 178us Leaving test module "runtime_configuration"; testing time: 220us *** No errors detected |
Another possibility for consuming the custom command line arguments would be from within a global fixture. This is especially useful when external parameters are needed for instantiating global objects used in the test module.
The usage is the same as for test cases. The following example runs the test module twice with different arguments, and illustrate the feature.
Tip | |
---|---|
The global fixture can check for the correctness of the custom arguments and may abort the full run of the test module. |
Code |
---|
#define BOOST_TEST_MODULE runtime_configuration2 #include <boost/test/included/unit_test.hpp> using namespace boost::unit_test; /// The interface with the device driver. class DeviceInterface { public: // acquires a specific device based on its name static DeviceInterface* factory(std::string const& device_name); virtual ~DeviceInterface(){} virtual bool setup() = 0; virtual bool teardown() = 0; virtual std::string get_device_name() const = 0; }; class MockDevice: public DeviceInterface { bool setup() final { return true; } bool teardown() final { return true; } std::string get_device_name() const { return "mock_device"; } }; DeviceInterface* DeviceInterface::factory(std::string const& device_name) { if(device_name == "mock_device") { return new MockDevice(); } return nullptr; } struct CommandLineDeviceInit { CommandLineDeviceInit() { BOOST_TEST_REQUIRE( framework::master_test_suite().argc == 3 ); BOOST_TEST_REQUIRE( framework::master_test_suite().argv[1] == "--device-name" ); } void setup() { device = DeviceInterface::factory(framework::master_test_suite().argv[2]); BOOST_TEST_REQUIRE( device != nullptr, "Cannot create the device " << framework::master_test_suite().argv[2] ); BOOST_TEST_REQUIRE( device->setup(), "Cannot initialize the device " << framework::master_test_suite().argv[2] ); } void teardown() { if(device) { BOOST_TEST( device->teardown(), "Cannot tear-down the device " << framework::master_test_suite().argv[2]); } delete device; } static DeviceInterface *device; }; DeviceInterface* CommandLineDeviceInit::device = nullptr; BOOST_TEST_GLOBAL_FIXTURE( CommandLineDeviceInit ); BOOST_AUTO_TEST_CASE(check_device_has_meaningful_name) { BOOST_TEST(CommandLineDeviceInit::device->get_device_name() != ""); } |
Output |
---|
# Example run 1 > runtime_configuration2 --log_level=all -- --some-wrong-random-string mock_device Running 1 test case... Entering test module "runtime_configuration2" test.cpp:46: info: check framework::master_test_suite().argc == 3 has passed test.cpp:47: fatal error: in "runtime_configuration2": critical check framework::master_test_suite().argv[1] == "--device-name" has failed [--some-wrong-random-string != --device-name] Leaving test module "runtime_configuration2" *** The test module "runtime_configuration2" was aborted; see standard output for details *** 1 failure is detected in the test module "runtime_configuration2" # Example run 2 > runtime_configuration2 --log_level=all -- --device-name mock_device Running 1 test case... Entering test module "runtime_configuration2" test.cpp:46: info: check framework::master_test_suite().argc == 3 has passed test.cpp:47: info: check framework::master_test_suite().argv[1] == "--device-name" has passed test.cpp:53: info: check 'Cannot create the device mock_device' has passed test.cpp:56: info: check 'Cannot initialize the device mock_device' has passed test.cpp:72: Entering test case "check_device_has_meaningful_name" test.cpp:74: info: check CommandLineDeviceInit::device->get_device_name() != "" has passed test.cpp:72: Leaving test case "check_device_has_meaningful_name"; testing time: 127us test.cpp:62: info: check 'Cannot tear-down the device mock_device' has passed Leaving test module "runtime_configuration2"; testing time: 177us *** No errors detected |
The above example instantiates a specific device through the DeviceInterface::factory
member function. The name of the
device to instantiate is passed via the command line argument --device-name
,
and the instantiated device is available through the global object CommandLineDeviceInit::device
. The module requires 3
arguments on the command line:
framework::master_test_suite().argv[0]
is the
test module name as explained in the previous paragraph
framework::master_test_suite().argv[1]
should
be equal to --device-name
framework::master_test_suite().argv[2]
should
be the name of the device to instantiate
As it can be seen in the shell outputs, any command line argument consumed
by the Unit Test Framework is removed from argc
/ argv
.
Since global fixtures are running in the Unit Test Framework
controlled environment, any fatal error reported by the fixture (through
the BOOST_TEST_REQUIRE
assertion) aborts
the test execution. Non fatal errors on the other hand do not abort the test-module
and are reported as assertion failure, and would not prevent the execution
of the test case check_device_has_meaningful_name
.
Note | |
---|---|
It is possible to have several global fixtures in a test module, spread over several compilation units. Each of those fixture may in turn be accessing a specific part of the command line. |
The initialization function are described in details in this section. The initialization function is called before any other test or fixture, and before entering the master test suite. The initialization function is not considered as a test-case, although it is called under the controlled execution environment of the Unit Test Framework. This means that:
BOOST_TEST
as it is not a test-case.
The following example shows how to use the command line arguments parsing described above to create/add new test cases to the test tree. It also shows very limited support to messages (does not work for all loggers), and error handling.
Code |
---|
#define BOOST_TEST_ALTERNATIVE_INIT_API #include <boost/test/included/unit_test.hpp> #include <functional> #include <sstream> using namespace boost::unit_test; void test_function(int i) { BOOST_TEST(i >= 1); } // helper int read_integer(const std::string &str) { std::istringstream buff( str ); int number = 0; buff >> number; if(buff.fail()) { // it is also possible to raise a boost.test specific exception. throw framework::setup_error("Argument '" + str + "' not integer"); } return number; } bool init_unit_test() { int argc = boost::unit_test::framework::master_test_suite().argc; char** argv = boost::unit_test::framework::master_test_suite().argv; if( argc <= 1) { return false; // returning false to indicate an error } if( std::string(argv[1]) == "--create-parametrized" ) { if(argc < 3) { // the logging availability depends on the logger type BOOST_TEST_MESSAGE("Not enough parameters"); return false; } int number_tests = read_integer(argv[2]); int test_start = 0; if(argc > 3) { test_start = read_integer(argv[3]); } for(int i = test_start; i < number_tests; i++) { std::ostringstream ostr; ostr << "name " << i; // create test-cases, avoiding duplicate names framework::master_test_suite(). add( BOOST_TEST_CASE_NAME( std::bind(&test_function, i), ostr.str().c_str() ) ); } } return true; } |
Output |
---|
# Example run 1 > runtime_configuration3 --log_level=all -- --create-parametrized 3 Running 3 test cases... Entering test module "Master Test Suite" test.cpp:59: Entering test case "name 0" test.cpp:17: error: in "name 0": check i >= 1 has failed [0 < 1] test.cpp:59: Leaving test case "name 0"; testing time: 179us test.cpp:59: Entering test case "name 1" test.cpp:17: info: check i >= 1 has passed test.cpp:59: Leaving test case "name 1"; testing time: 45us test.cpp:59: Entering test case "name 2" test.cpp:17: info: check i >= 1 has passed test.cpp:59: Leaving test case "name 2"; testing time: 34us Leaving test module "Master Test Suite"; testing time: 443us *** 1 failure is detected in the test module "Master Test Suite" # Example run 2 > runtime_configuration3 --log_level=all -- --create-parametrized Not enough parameters Test setup error: std::runtime_error: test module initialization failed # Example run 3 > runtime_configuration3 --log_level=all -- --create-parametrized dummy Test setup error: boost::unit_test::framework::setup_error: Argument 'dummy' not integer |
As seen in this example, the error handling is quite different than a regular test-case:
BOOST_TEST_ALTERNATIVE_INIT_API
),
the easiest way to indicate an error would be to return false
in case of failure.
std::runtime_error
or boost::unit_test::framework::setup_error
as above works as well.
It is possible to use the command line arguments to manipulate the dataset generated by a data-drive test case.
By default, datasets are created before entering the main
of the test module, and try to be efficient in the number of copies of their
arguments. It is however possible to indicate a delay for the evaluation
of the dataset by constructing the dataset with the make_delayed
function.
With the make_delayed
, the
construction of the dataset will happen at the same time as the construction
of the test tree during the test module initialization, and not before. It
is this way possible to access the master
test suite and its command line arguments.
The example below shows a complex dataset generation from the content of an external file. The data contained in the file participates to the definition of the test case.
Code |
---|
#define BOOST_TEST_MODULE runtime_configuration4 #include <boost/test/included/unit_test.hpp> #include <boost/test/data/test_case.hpp> #include <iostream> #include <functional> #include <sstream> #include <fstream> // this dataset loads a file that contains a list of strings // this list is used to create a dataset test case. class file_dataset { private: std::string m_filename; std::size_t m_line_start; std::size_t m_line_end; public: static const int arity = 2; public: file_dataset(std::size_t line_start = 0, std::size_t line_end = std::size_t(-1)) : m_line_start(line_start) , m_line_end(line_end) { int argc = boost::unit_test::framework::master_test_suite().argc; char** argv = boost::unit_test::framework::master_test_suite().argv; if(argc != 3) throw std::logic_error("Incorrect number of arguments"); if(std::string(argv[1]) != "--test-file") throw std::logic_error("First argument != '--test-file'"); if(!(m_line_start < std::size_t(-1))) throw std::logic_error("Incorrect line start/end"); m_filename = argv[2]; std::ifstream file(m_filename); if(!file.is_open()) throw std::logic_error("Cannot open the file '" + m_filename + "'"); std::size_t nb_lines = std::count_if( std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>(), [](char c){ return c == '\n';}); m_line_end = (std::min)(nb_lines, m_line_end); if(!(m_line_start <= m_line_end)) throw std::logic_error("Incorrect line start/end"); } struct iterator { iterator(std::string const& filename, std::size_t line_start) : file(filename, std::ios::binary) { if(!file.is_open()) throw std::runtime_error("Cannot open the file"); for(std::size_t i = 0; i < line_start; i++) { getline(file, m_current_line); } } auto operator*() const -> std::tuple<float, float> { float a, b; std::istringstream istr(m_current_line); istr >> a >> b; return std::tuple<float, float>(a, b); } void operator++() { getline(file, m_current_line); } private: std::ifstream file; std::string m_current_line; }; // size of the DS boost::unit_test::data::size_t size() const { return m_line_end - m_line_start; } // iterator over the lines of the file iterator begin() const { return iterator(m_filename, m_line_start); } }; namespace boost { namespace unit_test { namespace data { namespace monomorphic { template <> struct is_dataset<file_dataset> : boost::mpl::true_ {}; } }}} BOOST_DATA_TEST_CASE(command_line_test_file, boost::unit_test::data::make_delayed<file_dataset>( 3, 10 ), input, expected) { BOOST_TEST(input <= expected); } |
Output |
---|
# content of the file > more test_file.txt 10.2 30.4 10.3 30.2 15.987984 15.9992 15.997984 15.9962 # Example run 1 > runtime_configuration4 --log_level=all -- --test-file test_file.txt Running 2 test cases... Entering test module "runtime_configuration4" test.cpp:107: Entering test suite "command_line_test_file" test.cpp:107: Entering test case "_0" test.cpp:108: info: check input <= expected has passed Assertion occurred in a following context: input = 15.9879837; expected = 15.9991999; test.cpp:107: Leaving test case "_0"; testing time: 433us test.cpp:107: Entering test case "_1" test.cpp:108: error: in "command_line_test_file/_1": check input <= expected has failed [15.9979839 > 15.9961996] Failure occurred in a following context: input = 15.9979839; expected = 15.9961996; test.cpp:107: Leaving test case "_1"; testing time: 114us test.cpp:107: Leaving test suite "command_line_test_file"; testing time: 616us Leaving test module "runtime_configuration4"; testing time: 881us *** 1 failure is detected in the test module "runtime_configuration4" # Example run 2 > runtime_configuration4 --log_level=all -- --test-file non-existant.txt Test setup error: Cannot open the file 'non-existant.txt' # Example run 3 > runtime_configuration4 --log_level=all Test setup error: Incorrect number of arguments |
make_delayed
, the
tests generated from a dataset are instantiated during the framework
setup. This let the dataset generator access the argc
and argv
of the master
test suite.
std::logic_error
or boost::unit_test::framework::setup_error
can be raised and will be reported by the Unit Test Framework.