.. | ||
.gitignore | ||
.gitrepo | ||
build-googletest.sh | ||
component.mk | ||
Readme.md |
Mocking (low level bare-metal embedded) C code
Preparations
As Google test framework expect your test code to be written in c++, declaring your C functions as extern "C" is mandatory. Use#ifdef __cplusplus
extern "C" {
#endif
at the beginning of your headers and
#ifdef __cplusplus
}
#endif
at the end.
Keep in mind that STM32 is a 32 bit platform so unfortunately we cannot use amd64 for running our tests as pointer sizes would not match.
Creating stubs for external code
Unfortunately there is no way to make C functions dynamically mockable without source code modifications. We should use some macro magic to declare/define the functions need to be dynamically mockable.
Original code
foo.h:
...
uint8_t* Pu_GetTxBuffer(struct usartstatus_t *status);
...
foo.c:
...
uint8_t* Pu_GetTxBuffer(struct usartstatus_t *status)
{
return status->txBuffer.packet.payload;
}
...
Prepared for dynamic mocking
foo.h:
...
uint8_t* Pu_GetTxBuffer(struct usartstatus_t *status);
...
DECLARE_MOCK(Pu_GetTxBuffer);
...
foo.c:
...
uint8_t* MOCKABLE(Pu_GetTxBuffer)(struct usartstatus_t *status)
{
return status->txBuffer.packet.payload;
}
...
During normal build DECLARE_MOCK(Pu_GetTxBuffer)
expands to nothing and MOCKABLE(Pu_GetTxBuffer)
expands to Pu_GetTxBuffer
, so using them have no effect on the compiled binary.
When compiling the code for unit tests DECLARE_MOCK(Pu_GetTxBuffer)
will do two things:
- Declares a function with the same signature as
Pu_GetTxBuffer
namedPu_GetTxBuffer__
- Declares a function pointer named
test_GetTxBuffer
MOCKABLE
macro is the tricikiest part of the whole framework. It injects x86 assembly code into the intermediate assembly source generated from the C source to achieve the following:
Hijacks Pu_GetTxBuffer
and injects a code which check s the value of test_GetTextBuffer
pointer and calls the function it points to if the pointer is not NULL. Defines Pu_GetTxBuffer__
(using the original implementation) which is called in case test_GetTextBuffer
contains NULL.
As result we have an extra pointer for each prepared function to divert the execution when we need it and leave the original implementation in place when not.
Mocking/stubing platform code
Platform (like CMSIS, STM32 HAL or LL) has plenty of declarations and definitions necessary your code might be using. Unfortunately unit tests will need them too to compile. In most of the cases they even should be mockable. As the platform code written for ARM (sometimes even containing ARM assembly inserts) it would be extremely hard to make it compile on x86. there is no other way to make your own code compilable but copying necessary declarations and provide trivial (stub) definition in your own "fake" platform code.
It is also a good idea to make (at least parts of) your stub implementation mockable using the macros described above (see headers and sources in platforms/test/platform for example)
Writing your unit tests
There are several macros provided to make writing unit test as convinient as possible.
Defining mocks
Defining mock functions with no (void) return value:
DEFINE_MOCK(<function_name>, <decoration>, [<parameter list>]) {
<mock_function_implementation>
LEAVE_MOCK;
}
Example:
DEFINE_MOCK(LL_DMA_SetM2MDstAddress, mock, DMA_TypeDef *dma, uint32_t stream, uint32_t address) {
<implementation>
LEAVE_MOCK;
}
The above will generate the following C code:
static int LL_DMA_SetM2MDstAddress_mock_callcount;
static void LL_DMA_SetM2MDstAddress_mock(DMA_TypeDef *dma, uint32_t stream, uint32_t address) {
++LL_DMA_SetM2MDstAddress_mock_callcount;
{
<implementation>
}
}
Defining mock functions with return (non-void) value:
DEFINE_MOCK_RET(<return_type>, <function_name>, <decoration>, [<parameter list>]) {
<mock_function_implementation>
RETURN_MOCK_PREDEF(<function_name>, <decoration> | RETURN_MOCK(<function_name>, <decoration>, <return value>);
}
Example:
DEFINE_MOCK_RET(uint32_t, LL_USART_IsActiveFlag_IDLE, mock, USART_TypeDef *usart) {
<implementation>
RETURN_MOCK_PREDEF(LL_USART_IsActiveFlag_IDLE, mock)
}
The above will generate the following C code:
static int LL_USART_IsActiveFlag_IDLE_mock_callcount;
static uint32_t LL_USART_IsActiveFlag_IDLE_mock_retval;
static uint32_t LL_USART_IsActiveFlag_IDLE_mock(USART_TypeDef *usart) {
++LL_USART_IsActiveFlag_IDLE_mock_callcount;
{
<implementation>
}
return LL_USART_IsActiveFlag_IDLE_mock_retval;
}
Return value of the function above can be set by
MOCK_STORE(LL_USART_IsActiveFlag_IDLE, mock, retval, <value>);
or
MOCK_VAR(LL_USART_IsActiveFlag_IDLE, mock, retval) = <value>;
or
LL_USART_IsActiveFlag_IDLE_mock_retval = <value>;
during the test setup.
Helper variables for mocking
It is quite common that you need to store some data in global variables (can be checked later in from the test code or can be used by other mocks). There are few helper macros to make this easier. You can define a mock helper variable using
DEFINE_MOCK_VAR(<type>, <function_name>, <decoration>, <variable_name>);
Example:
DEFINE_MOCK_VAR(uint32_t, __set_PRIMASK, mock, lastprimask);
Which will expand to:
uint32_t __set_PRIMASK_mock_lastprimask;
You can access these variables using MOCK_VAR(<function_name>, <decoration>, <name>)
(e.g. if(MOCK_VAR(__set_PRIMASK, mock, lastprimask) != 0)
or MOCK_VAR(__set_PRIMASK, mock, lastprimask) = 1;
) but for setting the value of a mock helper variable you can also use MOCK_STORE(<function_name>, <decoration>, <varable_name>, <value>);
(e.g MOCK_STORE(__set_PRIMASK, mock, lastprimask, 1)
which is equivalent to setting the variable using MOCK_VAR
.
As it was descibed above, defining a mock function also defines (and administers) a call count variable for that function. For easier access of those variables we have MOCK_CALLCOUNT(<function_name>, <decoration>)
(e.g. if(MOCK_CALLCOUNT(__set_PRIMASK, mock) != 5) ...
)
Real-life example
Test writing using the infrastructure described above is quite straight-forward and easy. Let's assume we would like to write a unit test for the following function:
void MOCKABLE(Crc_AttachTasks)(struct crcstatus_t *status, struct crcslot_t *slot,
struct crctask_t *tasks, uint8_t taskCount)
{
slot->count = taskCount;
slot->tasks = tasks;
memset(tasks, 0, sizeof(*tasks)*taskCount);
uint32_t prim = __get_PRIMASK();
__disable_irq();
slot->next = status->firstSlot;
status->firstSlot = slot;
__set_PRIMASK(prim);
}
This function attaches a new tasks to one of the slots of CRC scheduler. As this ;lis is also processed from interrupt context it needs to disable interrupts for the period of the modification and restore the original interrupt enablement status on the end.
We can identify three platform specific function calls: __get_PRIMASK(), __disable_irq()
and __set_PRIMASK()
so we need stubs for them somewhere in the platform stub code:
Platform stub header:
void __disable_irq();
uint32_t __get_PRIMASK();
void __set_PRIMASK(uint32_t priMask);
Platform stub source:
void MOCKABLE(__disable_irq)() {}
uint32_t MOCKABLE(__get_PRIMASK)() { return 0; }
void MOCKABLE(__set_PRIMASK)(uint32_t primask) {}
In our test code we need to mock these function (making possible to verify that they're called as modification of the linked list of slots need to be guarded against interrupts)
uint32_t effective_primask = 0;
DEFINE_MOCK(__set_PRIMASK, mock, uint32_t primask) {
effective_primask = primask;
LEAVE_MOCK;
}
DEFINE_MOCK_RET(uint32_t, __get_PRIMASK, mock) {
RETURN_MOCK(__get_PRIMASK, mock, effective_primask);
}
DEFINE_MOCK_VAR(crcslot_t *, __disable_irq, mock, firstslot_required);
DEFINE_MOCK(__disable_irq, mock) {
if(MOCK_CALLCOUNT(__disable_irq, mock) < 2) {
EXPECT_EQ(crcStatus.firstSlot, MOCK_VAR(__disable_irq, mock, firstslot_required));
}
effective_primask = 1;
LEAVE_MOCK;
}
With these mock functions we actually mock the behaviour of he ARM Cortex PRIMASK register API. We also add some check to _disable_irq_mock()
that verifies that the firstSlot member of the crcStatus has not changed before disabling interrupts.
Now we prepared everything for writing our first unit test for Crc_AttachTasks function:
TEST(CrcScheduler, AttachTask_single) {
DMA1 = &dma1;
DMA2 = &dma2;
effective_primask = 0;
Crc_InitStatus(&crcStatus, &fakeCrc, DMA2, LL_DMA_STREAM_4);
ACTIVATE_MOCK_RV(__get_PRIMASK, mock, 0);
ACTIVATE_MOCK(__set_PRIMASK, mock);
ACTIVATE_MOCK(__disable_irq, mock);
MOCK_STORE(__disable_irq, mock, firstslot_required, nullptr);
Crc_AttachTasks(&crcStatus, &slot1, tasks1, 2);
EXPECT_EQ(MOCK_CALLCOUNT(__get_PRIMASK, mock), 1);
EXPECT_EQ(MOCK_CALLCOUNT(__set_PRIMASK, mock), 1);
EXPECT_EQ(MOCK_CALLCOUNT(__disable_irq, mock), 1);
EXPECT_EQ(crcStatus.firstSlot, &slot1);
EXPECT_EQ(slot1.next, nullptr);
EXPECT_EQ(slot1.count, 2);
EXPECT_EQ(crcStatus.activeSlot, nullptr);
}
There are two ways to activate a mock:
ACTIVATE_MOCK(<function>, <decoration>);
and
ACTIVATE_MOCK_RV(<function>, <decoration>, <return_value>)
Both macros reset the corresponding callcount
variable of the mock function to zero and divert the mocked function to the mock. In addition to this ACTIVATE_MOCK_RV
also sets the return value variable of the mock function (created by DEFINE_MOCK_RET
) to the supplied value. This can be used to define the return valuse of the mock (if the test writer decides to write the mock function this way).
After preparing everything for the test wi actually call Crc_AttachTasks
with the appropriate parameters then verifying the results.