Component of the Week #10: bsls_asserttest
- Summary:
Provides a facility to test assertion macros.
High-quality software is recognizable by employing a wide range of techniques to maximize its reliability, maintainability, and usability. Key to all of these aspects are thorough testing and careful deployment of defensive programming.
BDE itself is thoroughly tested, and supports applying the same testing methodology to any other codebase — see Testing for more on that topic. A robust facility for verifying that a library’s API is being used correctly (without imposing overhead on production uses of that same API) is available through the macros in the bsls_assert and bsls_review components.
Combining these, however, is challenging. Thorough testing of software demands that each potential behavior of a library — both guaranteed to clients and not — be tested. This testing must, therefore, include testing of assertions that check a function’s preconditions. These assertions, however, can be compiled with a number of different flags that alter their behavior, ranging from being checked (and invoking the installed assert or review handler) to being ignored or even assumed.
To manage testing assertions correctly based on how testable they actually are in a given build, BDE provides another important component — bsls_asserttest. This component contains macros that enable testing assertions without running the functions out-of-contract in builds where the assertions are not enabled.
Consider our go-to example of a function with narrow preconditions, good old sqrt:
#include <bsls_assert.h>
#include <cmath>
/// Return the square root of the specified `value`; the behavior is
/// undefined unless `value >= 0`.
double sqrt(double value)
{
BSLS_ASSERT(value >= 0);
return std::sqrt(value);
}
Now we can test the assertion of the precondition by
passing various inputs to sqrt. To use bsls_asserttest to verify that
our assertion handles in-contract behavior properly we just create the
appropriate guard and use the macro BSLS_ASSERTTEST_ASSERT_PASS. When
assertions are enabled, we will detect a bug if the assertion does
not allow through some valid input. Whether they are enabled or not,
we’ll still harmlessly invoke the function in-contract (and discard the
result).
void testSqrt1()
{
bsls::AssertTestHandlerGuard g;
BSLS_ASSERTTEST_ASSERT_PASS( sqrt(0.0) );
BSLS_ASSERTTEST_ASSERT_PASS( sqrt(1.0) );
BSLS_ASSERTTEST_ASSERT_PASS( sqrt(17.0) );
}
Testing out-of-contract behavior, however, is much trickier. If we
call the function with bad inputs and the assertion macros are disabled,
we then wander into arbitrary undefined behavior within our implementation,
which does not make for particularly reliable test drivers. Once
again, bsls_asserttest provides us with macros that will do
the right thing — we assert that certain functions will fail
with an assertion violation when assertions are enabled:
void testSqrt2()
{
bsls::AssertTestHandlerGuard g;
BSLS_ASSERTTEST_ASSERT_FAIL( sqrt(-1.0) );
BSLS_ASSERTTEST_ASSERT_FAIL( sqrt(-17.0) );
}
When assertions are not enabled, these tests will do nothing — not even call our function with the specified parameters. When the assertions are enabled, however, these tests will fail if an assertion violation is not detected — i.e., if the function runs to completion and returns normally. There will also be a test failure if an assertion is hit in a component that is not the component under test. This testing that preconditions are being asserted properly is a testing paradigm commonly known as “negative testing”.
If you’d like to know more, check out the documentation for bsls_asserttest for details. There’s more there like:
How to test that assertions of different levels (such as
_OPTor_SAFE) will be hit.How to test wrappers that might rely on preconditions checks in a different component using the
_RAWvariants of the macros.