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:
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? 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:
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.
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.
I use Python and Behave here, to build, download, and run the whole application on the target.
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.
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.
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!