Unit-testing in a hostile environment, Jacek Puchta

Some time ago I’ve read a blog post what if programming languages were vehicles, where plain C was compared to an old-school military jeep WillysMC, and C++ was a brand-new Humvee (High Mobility Multipurpose Wheeled Vehicle) – I find it a very nice parallel. It inspired me to the afterthought:

Sometimes one cannot freely choose the technology in which the product has to be developed. It is not a big challenge to operate successfully using a top-notch technology.

c++ as a vehiclec as a vehicleBut what if you are put in a hostile environment, and instead of a Humvee you have to operate with a run down jeep?

Real life example

We have to develop extensions of a library used by our client. For various reasons using another library is not an option. The library is over 1 MLOC of spaghetti and very legacy code, written in plain C. There are no automated tests of the library (no unit-tests, no integration tests, no tests at all).

However, we would like to write unit tests like:

TEST_F(DivideShould, round_to_closest_int_on_config_flag_SHOULD_ROUND_on) {
    EXPECT_CALL(checkConfigFlagMock.get(), checkConfigFlag(SHOULD_ROUND))
            .WillRepeatedly(Return(TRUE));
    EXPECT_EQ(3, divide(5, 2));
}

TEST_F(DivideShould, round_down_on_config_flag_SHOULD_ROUND_off) {
    EXPECT_CALL(checkConfigFlagMock.get(), checkConfigFlag(SHOULD_ROUND))
            .WillRepeatedly(Return(FALSE));
    EXPECT_EQ(2, divide(5, 2));
}

Moreover, we would like these tests to be light-weight and fast-building, so that we could use TDD.

Encountered problems

An obvious solution would be: to create a separate standalone small library with our new feature. Unfortunately this is not an option for two reasons:

  • There are two-way dependencies (we depend on some parts of the legacy lib, the other parts of the legacy lib depend on us, the legacy lib cannot be split).
  • We cannot modify the build toolchain of the legacy library.

Lack of access to the build toolchain is a source of another problem – building the legacy library is extremely slow.

Furthermore, the code needs to work on WinCE 6.0. Thus it can be build only by a VisualStudio. However we would like to develop (and run unit-tests) on linux.

How did we deal with it

  • We followed several “good practices” – to isolate our new code as much as possible, and thus to make it testable.
  • We implemented a few hacks to cut the dependencies.
  • We accepted, that there are parts of code, that cannot be tested (unless abnormally large effort for refactoring the legacy code were taken).

Good practices

  • Keep the platform-specific code (for example: sockets, filesystem, etc.) in separate modules.
    These modules will not be unit-testable under Linux, but well, we cannot have everything.

    • Whenever possible – do not expose platform-specific types (e.g. `SOCK`) in headers. It makes all the files including such header becoming platform-specific.
  • One module – one “class”.
    • Use static functions – they serve as private methods.
    • Use conventions when you do not have access control.
      • We have a structure that contains some fields we consider private, but the structure has to be published in the header of the module.
      • The rule of thumb – the fields starting with underscore should be accessed only by functions from the module, where the structure was defined.

Hack #1: Parallel CMake-based build toolchain for unit-tests

As it was mentioned – we do not own the build toolchain and the solution must work on WinCE 6.0. This complicates working with unit-tests, since building requires compiling the entire 1MLOC solution and running unit-tests requires deployment on an archaic machine.

In order to workaround these problem we simply pretend, that our additional code is a separate, platform-independent library.

  • write CMake scripts for building our small library (let’s call it extensionLib)
    • Obviously we omit the platform-dependent modules.
  • we write CMake script for additional library (let’s call it legacy-stub). It has an API consisting of these functions from the original legacy library, that we has to use.
    • We write the stub implementations of the functions from legacy-stub. These functions will not be called anyway (see the Hack #2), they are only to make extensionLib linkable.
  • We write CMake scripts for unit-tests. The unit-tests depend on extensionLib, which depends on legacy-stub.
    Since legacy-stub is a stub, and thus it is portable, the unit-tests can be build on our workstations, using gcc.

Hack #2: Poor-man’s dependency-injection pattern

If a new module depends on the legacy-functions, we do not call them directly. Instead we introduce pointer to each legacy-function (being a static field of the module) and refer to these functions only via the pointer.

Moreover we introduce an additional function called _EnableTestMode() or similar, that substitutes the pointers to legacy functions. This function is called in the constructor of test-case class so that it sets the pointers to the desired methods of mock classes.

Example

  • A module divide.c should implement a function, that divides two integer number.
  • Depending on the configuration flag SHOULD_ROUND , the result should be either rounded to the closest integer (i.e. 2,5 goes to 3, while 2,499 goes to 2), or rounded always down (as the standard / operation in C).
  • The configuration is managed by the legacy library. We has to access it using the legacy function:
    BoolType LegacyReadConfigBit(ConfigKeyType configKey);
  • We want to have unit-tests of the module divide.c
In the header divide.h
typedef BoolType (*checkConfigFlagFunc)(ConfigKeyType configKey);
/* BoolType and ConfigKeyType are types from the legacy library.  *
 * Keep in mind that in plain C there is no build-in bool type.   */
void _EnableTestMode(checkConfigFlagFunc checkConfigFlagMock);

int divide(int dividend, int divider);
In the divide.c
static checkConfigFlagFunc checkConfigFlag = LegacyReadConfigBit; /* The legacy function */

void _EnableTestMode(checkConfigFlagFunc checkConfigFlagMock) {
    checkConfigFlag = checkConfigFlagMock;
}

/* And in the code of other functions refer only to `checkConfigFlag()`: */

int divide(int dividend, int divider) {
    if (checkConfigFlag(SHOULD_ROUND)) {
        return sgn(dividend) * sgn(divider) * (abs(dividend) + (abs(divider) >> 1) ) / abs(divider);
    } else {
        return dividend / divider;
    }
}
In the tests divide_test.cpp
#include "gtest/gtest.h"
#include "gmock/gmock.h"

#include "boost/optional.hpp"
#include "boost/utility/typed_in_place_factory.hpp"

extern "C" {
#include "divide.h"
} // extern "C"

using namespace testing;
class CheckConfigFlagMock {
public:
    MOCK_METHOD1(checkConfigFlag, BoolType(ConfigKeyType));
};

class DivideShould: public testing::Test {
protected:
    DivideShould() {
        checkConfigFlagMock = boost::in_place<CheckConfigFlagMock>();
        _EnableTestMode(DivideShould::checkConfigFlagMockFunc);
    }

    ~DivideShould() {
        checkConfigFlagMock.reset(); // In order to destroy the mock, and thus verify the calls.
    }

    static boost::optional<CheckConfigFlagMock> checkConfigFlagMock;
private:
    static BoolType checkConfigFlagMockFunc(ConfigKeyType configKey) {
        return checkConfigFlagMock ?
                 checkConfigFlagMock.get().checkConfigFlag(configKey)
               : LegacyReadConfigBit(configKey);
    }
}

boost::optional<CheckConfigFlagMock> DivideShould::checkConfigFlagMock;

TEST_F(DivideShould, round_to_closest_int_on_config_flag_SHOULD_ROUND_on) {
    EXPECT_CALL(checkConfigFlagMock.get(), checkConfigFlag(SHOULD_ROUND))
            .WillRepeatedly(Return(TRUE));
    EXPECT_EQ(3, divide(5, 2));
}

TEST_F(DivideShould, round_down_on_config_flag_SHOULD_ROUND_off) {
    EXPECT_CALL(checkConfigFlagMock.get(), checkConfigFlag(SHOULD_ROUND))
            .WillRepeatedly(Return(FALSE));
    EXPECT_EQ(2, divide(5, 2));
}

Your email address will not be published. Required fields are marked *

*

div#stuning-header .dfd-stuning-header-bg-container {background-image: url(http://9livesdata.com/wp-content/uploads/2017/05/thomas-kvistholt-191153-e1496408091346.jpg);background-size: cover;background-position: center center;background-attachment: scroll;background-repeat: no-repeat;}#stuning-header div.page-title-inner {min-height: 100px;}