c++cunit-testingembeddedgoogletest

Google Test for embedded systems


I would like to write unit tests for my embedded application software using Google Tests.

These tests would be performed on the application software, which is written in C++. The drivers being used by the application software (eg. I2C, SPI), fault assertion are written in C. My questions would be:

  1. What would be a good place to start off? I mean resources I could read to learn more about using Google Test in an embedded environment.
  2. How do I go about mocking my driver files? For example, if I have a void read(uint8_t address) function, within my I2C library, how do I go about mocking this function, so that this particular function is called within my C++ class?
  3. These driver files written in C are also included in my C++ files. I tried compiling a bare Test file, only including my C++ class header, and had compilation issues, since the compiler couldn't find the driver header. How can I avoid this issue?
  4. Managing failed assertions with the code - Failed assertions within my driver library, calls for a system reset. How can I emulate this within the tests?

Solution

  • I've recently tested a FAT file system and bootloader implementation with gTest (GoogleTest) for an Arm Cortex-M3 core, so I'll leave my two cents.

    Embedded software testing presents the problem that it's impossible to replicate the HW environment through mocking. I came up with three sets of tests:

    Unit tests on the host

    These are unit tests (that I write with TDD) that run on my PC. I use these tests to develop my application logic. This is where I need mocking/stubbing. My company uses a hardware abstraction layer (HAL), and that's what I mock. This last bit is fundamental if you want to write testable code.

    /* this is not testable */
    my_register->bit0 = 1;
    
    /* this is also not testable */
    *my_register |= BIT0;
    

    Don't do direct register access, use a simple HAL wrapper function that can be mocked:

    /* this is testable */
    void set_bit(uint32_t* reg, uint8_t bit)
    {
        *reg |= bit;
    }
    
    set_bit(my_register , BIT0);
    

    The latter is testable because you're going to mock the set_bit function, thus breaking the dependency on the HW.

    Unit tests on the target.

    This is a much smaller set of tests than the host unit tests, but they're still useful especially for testing drivers and the HAL functions. The idea behind these tests is that I can properly test the functions that I'll mock. Because this runs on the target, I need this as simple and lightweight as possible, so I use MinUnit, which is a single C header file. I've run on-target tests with MinUnit on a Cortex-M3 core and on a proprietary DSP code (without any modifications). I've also used TDD here.

    Integration tests

    I use Python and Behave here, to build, download, and run the whole application on the target.

    Answering your questions

    1. As others have already said, start with gTest Primer, and don't worry about mocking, just getting the hang of using gTest. A good alternative that offers some memory checking (for leaks) is Cpputest. I have a slight preference for the gTest syntax for deriving the setup classes. Cpputest can run tests written with gTest. Both are great frameworks.

    2. I used Fake Function Frakework for mocking and stubbing. It's ridiculously simple to use and it offers everything expected from a good mocking framework: setting different return values, passing callbacks, checking the argument call history, etc. I want to give Ceedling a go. So far FFF has been great.

    3. I don't do that. I compile the testing framework and my tests with a C++ compiler (g++ in my case) and my embedded code with a C compiler (gcc), and just link them together. From the example below, you'll see that I don't include C++ headers in C files. When linking your tests, you'll link everything but the C source files for the functions you're mocking.

    Managing failed assertions with the code - Failed assertions within my driver library, calls for a system reset. How can I emulate this within the tests?

    I'd mock the reset function, adding a callback to "reset" whatever you need.

    Say that you want to test a read_temperature function that uses the read function. Below is a gTest example that uses FFF for mocking.

    hal_i2c.h

    /* HAL Low-level driver function */
    
    /**
     * @brief Read a byte from the I2C bus.
     * 
     * @param address The I2C slave address.
     * @return uint8_t The data read from the I2C bus.
     */
    uint8_t read(uint8_t address);
    

    read_temperature.h

    /**
     * @brief Read the temperature from the sensor in the board
     * 
     * @return float The temperature in degrees Celsius.
     */
    float read_temperature(void);
    

    read_temp.c

    #include <hal_i2c.h>
    
    float read_temperature(void)
    {
        unit8_t raw_value;
        float temp;
    
        /* Read the raw value from the I2C sensor at address 0xAB */
        raw_value = read(0xAB);
    
        /* Convert the raw value to Celcius */
        temp = ((float)raw_value)/0.17+273;
        return temp;
    }
    

    test_i2c.cpp

    #include <gtest/gtest.h>
    #include <fff.h>
    
    DEFINE_FFF_GLOBALS;
    
    extern "C"
    {
    #include <hal_i2c.h>
    #include <read_temperature.h>
    
    // Declare the fake C functions using FFF. This needs be inside the extern "C"
    // block because we're mocking a C function and the C++ name-mangling would
    // break linking otherwise.
    
    
    // Create a mock for the uint8_t read(uint8_t address) function.
    FAKE_VALUE_FUNC(uint8_t , read, uint8_t);
    }
    
    TEST(I2CTest, test_read) {
    
        // This clears the FFF counters for the fake read() function
        RESET_FAKE(read);
    
        // Set the raw temperature value the fake for read should return
        read_fake.return_val = 0xAB;
    
        // Make sure that we read 123.4 degrees
        ASSERT_EQ((float)123.4, read_temperature());
    }
    

    For more complex test scenarios with test classes, you can call RESET_FAKE in the SetUp() method.

    Hope this helps! Cheers!