Testing and Debugging

Debugging code that uses Boost libraries follows the same general principles as debugging any other C++ code, but with the added assistance of test and logging specific libraries, and integration of these libraries into popular tools.

Debugging Strategies

Here are some strategies to consider:

  1. Understanding the Library: Familiarize yourself with the specific Boost libraries that you’re using. Boost is a large and diverse collection of libraries, and each library may have unique behaviors, requirements, or quirks. Understanding the libraries you’re using will help you spot issues more easily.

  2. Read the Boost Library Documentation: Boost has extensive and well-maintained documentation. If you’re having trouble with a specific library or function, start by looking up its documentation.

  3. Use a Debugger: Tools like gdb or lldb on Unix-like systems, or the Visual Studio debugger on Windows, can be incredibly useful. You can step through your code line by line, inspect variables at any point, and generally see exactly what your code is doing.

  4. Compile Warnings and Errors: Boost code will often make extensive use of templates, and errors in template code can sometimes result in complex and confusing compiler error messages. When confronted with such a message, don’t panic. Start at the top and try to decipher what the compiler is telling you. Often, the first few lines of the error message will contain the key to understanding the problem.

  5. Unit Testing: Boost provides a testing framework, Boost.Test, which you can use to write unit tests for your code. Writing tests can help you catch errors and regressions, and it can also help you understand your code better. Boost.Test is integrated and available when using Microsoft Visual Studio. Refer to the Using Boost.Test section below.

  6. Logging and Debug Output: Sometimes, it can be useful to have your program output diagnostic information while it’s running. You can use std::cerr, std::clog, or a logging library to output information about your program’s state at key points.

  7. Code Review: If you’re still stuck, consider asking a fellow developer to review your code. Sometimes, a fresh pair of eyes can spot issues that you might have missed.

  8. Online Communities: If you’re still stuck after trying the above steps, you can ask for help online. The Boost developers community is large and generally very helpful. There are forums, mailing lists, and Stack Overflow where you can ask for help.

Remember, debugging is a skill that gets better with practice. The more you work with the Boost libraries, the more you’ll learn about their idiosyncrasies and the better you’ll become at debugging issues with them.

Using Boost.Test

Boost.Test is a robust, powerful library designed to facilitate writing unit tests in C++. It provides a framework for creating, managing, and running tests, enabling developers to ensure that their code functions as expected.

To start using the Test library, include its header in your test file:

#define BOOST_TEST_MODULE MyTest
#define BOOST_TEST_DYN_LINK
#include <boost/test/unit_test.hpp>

This will allow dynamic linking to the test library. The BOOST_TEST_MODULE macro creates a main function for your test executable, meaning you don’t need to write one yourself.

Boost.Test uses "test cases" for testing. A test case is a function that performs the test. You can define one using the BOOST_AUTO_TEST_CASE(test_case_name) macro. The macro parameter becomes the test case’s name. For example:

BOOST_AUTO_TEST_CASE(MyTestCase) {
    BOOST_TEST(true); // A simple test that always passes
}

In this example, MyTestCase is a simple test case. The BOOST_TEST macro checks its argument and, if it’s false, reports an error.

Boost.Test provides a set of macros for different assertions:

  • BOOST_TEST for basic testing.

  • BOOST_CHECK for non-critical conditions where the test continues even if the check fails.

  • BOOST_REQUIRE for critical conditions where the test is aborted if the condition fails.

The suite feature is another strength. It allows you to group test cases, making your tests more organized and manageable. To create a suite, you can use the BOOST_AUTO_TEST_SUITE(suite_name) macro:

BOOST_AUTO_TEST_SUITE(MyTestSuite)

BOOST_AUTO_TEST_CASE(TestCase1) {
    // Test code here
}

BOOST_AUTO_TEST_CASE(TestCase2) {
    // Test code here
}

BOOST_AUTO_TEST_SUITE_END()

In this snippet, MyTestSuite is a test suite that contains TestCase1 and TestCase2.

Another powerful feature is the fixture. Fixtures are useful when you want to perform setup and teardown operations for your tests. You can create a fixture class and use BOOST_FIXTURE_TEST_CASE to apply it to a test case:

struct MyFixture {
    MyFixture() {
        // Setup code here
    }

    ~MyFixture() {
        // Teardown code here
    }
};

BOOST_FIXTURE_TEST_CASE(TestCaseWithFixture, MyFixture) {
    // Test code here
}

In this example, MyFixture is a fixture class. Its constructor and destructor are called before and after TestCaseWithFixture, respectively.

Boost.Test also supports parameterized and data-driven tests, exception handling, and custom log formatting.

To compile and run your tests, use your preferred C++ compiler to compile the test source file and the Test library. Then, run the resulting executable to execute your tests.

Using Boost.Log

Logging can be a helpful part of debugging, so consider using Boost.Log, as it provides a flexible and customizable logging system. It allows you to log messages from different parts of your application to various targets (e.g., console, file, etc.) and with different severity levels or categories.

Here is a simple example of how you might use Log:

#include <boost/log/trivial.hpp>

int main(int, char*[])
{
    BOOST_LOG_TRIVIAL(trace) << "A trace severity message";
    BOOST_LOG_TRIVIAL(debug) << "A debug severity message";
    BOOST_LOG_TRIVIAL(info) << "An informational severity message";
    BOOST_LOG_TRIVIAL(warning) << "A warning severity message";
    BOOST_LOG_TRIVIAL(error) << "An error severity message";
    BOOST_LOG_TRIVIAL(fatal) << "A fatal severity message";

    return 0;
}

In this example, BOOST_LOG_TRIVIAL is a simple macro that logs a message with a specified severity level.

Severity levels are provided for log messages that you can use to indicate the importance or urgency of different logs. In basic usage, these severity levels are represented by an enumeration type.

In the example provided above, the severity levels are defined as follows:

namespace trivial = boost::log::trivial;
enum severity_level
{
    trace,
    debug,
    info,
    warning,
    error,
    fatal
};

Each of these levels can be used to log messages of different importance:

  1. trace: Very detailed logs, typically used for debugging complex issues.

  2. debug: Detailed logs useful for development and debugging.

  3. info: Information about the normal operation of the program.

  4. warning: Indications of potential problems that are not immediate errors.

  5. error: Error conditions that may still allow the program to continue running.

  6. fatal: Severe errors that may prevent the program from continuing to run.

You can customize these levels to fit your app, and you can also filter logs based on their severity level. For example, in a production environment, you might ignore trace and debug logs and only record info, warning, error, and fatal logs.

Other Libraries

Other libraries that might help you with testing and debugging include:

  • Boost.Stacktrace: Stacktrace can be used to capture, store, and print sequences of function calls and their arguments. This can be a lifesaver when you need to debug complex code or post-mortem crashes.

  • Boost.Exception: This library enhances the error handling capabilities of C++. It enables attaching arbitrary data to exceptions, transporting of exceptions between threads, and more, thereby providing richer error information during debugging.

  • Boost.StaticAssert: It provides a macro, BOOST_STATIC_ASSERT, which can be used to perform assertions that are checked at compile time rather than at run time. This can be used to catch programming errors as early as possible.

  • Boost.Bind and Boost.Lambda: These libraries allow for the creation of small, unnamed function objects at the point where they are used. These can be useful in writing concise tests.

  • Boost.Mp11: A MetaProgramming Library, though not exclusively for testing or debugging, this library can be helpful in writing compile-time tests.

Boost.Test Tutorial

This topic is a step-by-step tutorial on how to get going with the Boost.Test library. This is a very substantial library with lots of functions and documentation. It is valuable to understand the concept of adding tests to a simple program, before venturing further.

In this tutorial, we are using Microsoft Visual Studio, on Windows.

One question you may have is "what happens to the main function?" When using Boost.Test, the main function is implicitly defined by the library. This is one of the features that Boost.Test offers to simplify the process of writing tests. This is done using the preprocessor directive #define BOOST_TEST_MODULE. There is the option for you to use your own custom main function, which we will come to in a later tutorial. The automatic generation of the main function does allow you to focus on writing the test cases themselves.

Let’s get started with a trivial example.

Trivial Example

Here’s a very simple example of a test suite with Boost.Test:

  1. Use Visual Studio to create a C++ Console application, call it something like "Trivia".

  2. In the project Properties for C++/General, locate Additional Include Directories and add the path to your Boost libraries. The path will be something like C:\Users\<your path>\boost_1_81_0.

  3. Then, still in Properties, but now for Linker/General add to the Additional Library Directories with the path to your Boost lib folder. This path will be something like C:\Users\<your path>\lib.

  4. In the Project menu, select Add New Item, and locate Boost.Test. Add it to your project.

  5. Replace the boilerplate code of test.cpp with:

    #define BOOST_TEST_MODULE MyTestSuite
    #include <boost/test/included/unit_test.hpp>
    
    BOOST_AUTO_TEST_CASE(MyTestCase)
    {
        BOOST_CHECK(1 + 1 == 2);
    }
  6. Comment out the main function. In this example, the main function is automatically generated by #include <boost/test/included/unit_test.hpp> when BOOST_TEST_MODULE is defined.

  7. Run the program. You should get the *** No errors detected message, as one plus one does equal two!

  8. Change the code to:

    BOOST_AUTO_TEST_CASE(MyTestCase)
    {
        BOOST_CHECK(1 + 1 == 3);
    }
  9. Run the program. Do you now get a red error message!

This trivial example shows the two kinds of messages we might get. Now let’s move onto something with a bit more meat.

String Reversal Example

  1. Use Visual Studio to create a C++ Console application, call it something like "StringRev".

  2. In the project Properties for C++/General, locate Additional Include Directories and add the path to your Boost libraries. The path will be something like C:\Users\<your path>\boost_1_81_0.

  3. Then, still in Properties, but now for Linker/General add to the Additional Library Directories with the path to your Boost lib folder. This path will be something like C:\Users\<your path>\lib.

  4. Replace the boilerplate code in the .cpp file to:

    #include <string>
    #include <iostream>
    
    std::string revString(std::string str)
    {
        int n = (int) str.length();
    
        for (int i = 0; i < n / 2; i++) {
            std::swap(str[i], str[n - i - 1]);
        }
        return str;
    }
    
    int main(int argc, char* argv[])
    {
        std::cout << revString("Reverse String Function") + "\n";
        std::cout << revString("Even") + "\n";
        std::cout << revString("Odd") + "\n";
    }
  5. Run the program, and ensure you get the three strings reversed appearing in a Console window.

    rev string test
  6. Good, now let’s see how you would add Boost.Test functions to this.

  7. In the Project menu, select Add New Item, and locate Boost.Test. Add it to your project.

  8. Comment out your main function, as it will be replaced with the Boost.Test main function, whilst the tests are running.

  9. Add the following automatic tests to your .cpp file:

    BOOST_AUTO_TEST_CASE(check_revString) {
        BOOST_TEST(revString("abcd") == "dcba");
        BOOST_TEST(revString("12345") == "54321");
        BOOST_TEST(revString("Even") == "nevE");
    
        // Add a failure case
        BOOST_TEST(revString("Odd") == "DDO");
    }
  10. Run the program. Do you get one error: check revString("Odd") == "DDO" has failed?

  11. Correct the error by changing "DDO" to "ddO" in your code.

  12. Run the program again. Do you now get *** No errors detected? If so great, the tests have worked.

  13. Perhaps add test cases to the BOOST_AUTO_TEST_CASE function, to check the case of an empty string, and for a single character string:

        BOOST_TEST(revString("a") == "a");
        BOOST_TEST(revString("") == "");
  14. Add and test any other strings that come to mind.

Next Steps

You can imagine now how you can add unit tests to your existing projects, checking the correct working of many of your functions.

And check out the full functionality of Boost.Test.