Not every function will be as easy as a simple arithmetic operations, unfortunately we are writing code in C, that means arrays, pointers, structures and even global variables of any kind are our everyday pain in the b#$#$. So lets see how to test them all.
I’m going to avoid writing app.dummy.h in most of the examples, I assume you already know you have to place the prototypes the functions we made, Also i will avoid to write the entire contents of the files, to make shorter the code examples
Testing functions with global variables
This time we are going to refer a global variables like the ones who has an external reference in our codes, like in this example variable subs, that later is use in function subtraction
app/dummy.c
uint32_t subs;
uint32_t substraction( uint32_t a )
{
return a - subs;
}
as we mention our header file makes reference to variable subs as extern
app/dummy.h
#ifndef __DUMMY_H__
#define __DUMMY_H__
extern uint32_t subs;
uint32_t substraction( uint32_t a );
#endif // __DUMMY_H__
In our test case we gave an initial value to variable subs to later run the unit test. As you can see there is nothing out of this world testing global variables, but things will get more interesting the more complexity we add in our codes, stay tunes…
test/test_dummy.c
void test__substraction__two_integers(void)
{
/*We give an initial value to our global variable, ceedling knows about
the variable because the external reference in dummy.h*/
subs = 10;
/*call function under test that make use of global function*/
uint32_t res = substraction( 20 );
TEST_ASSERT_EQUAL_MESSAGE( 10, res, "20 - 10 = 10" );
}
Testing arrays
Most of the time arrays are passed and return trough pointers to a functions.
app/dummy.c
/*The function receives and array to be filled with incremental values from zero to size-1*/
void fill_array( uint8_t *array, uint8_t size )
{
for( uint8_t i = 0 ; i < size ; i++ )
{
array[i] = i;
}
}
test/test_dummy.c
void test__fill_array__should_fill_array_with_incrementing_values(void)
{
/*declare the expected results values in the array*/
const uint8_t result[] = { 0, 1, 2, 3, 4 };
/*array to be fill with incremental values from 0*/
uint8_t array[5];
/*fucntion under test*/
fill_array( array, 5 );
/*The macro allows to compare a memory region pointed by first parameter against another one
pointed by second parameter of size indicated by the third paramter*/
TEST_ASSERT_EQUAL_MEMORY_MESSAGE( result, array, 5, "Array is not filled with incremental values" );
}
We can also use a more specific macro to test arrays depending on its data elements type, like:
void test__fill_array__should_fill_array_with_incrementing_values(void)
{
/*declare the expected results values in the array*/
const uint8_t result[] = { 0, 1, 2, 3, 4 };
/*array to be fill with incremental values from 0*/
uint8_t array[5];
/*fucntion under test*/
fill_array( array, 5 );
/*The macro allows to compare a memory region pointed by first parameter against another one
pointed by second parameter of size indicated by the third paramter*/
TEST_ASSERT_EQUAL_INT8_ARRAY_MESSAGE( result, array, 5, "Array is not filled with incremental values" );
}
Testing Pointers
To test pointers you have to remember the concept of pointer, “It is a variable that store a memory address“. and to be honest there is no black magic testing these buddies, Ceedling offer a s[special macro to test memory address TEST_ASSERT_EQUAL_PTR
or TEST_ASSERT_EQUAL_PTR_MESSAGE
app/dummy.c
/*simple function to find a character in a string*/
char *token_in_string( char *ptr, char var )
{
for( i=0; i<strlen(ptr); i++ )
{
if( ptr[i] == var )
return &ptr[i];
}
return NULL;
}
test/test_dummy.c
void test__token_in_string_find_first_letter_a( void )
{
/*string to test*/
char string[] = "Hola";
char *pointer;
/*the function hsall return the address of letter 'a' in string*/
pointer = token_in_string( string, 'a' );
TEST_ASSERT_EQUAL_PTR_MESSAGE( &string[3], pointer, "character a was not found in string" );
}
Testing Structures
Basically the same as testing arrays with macro TEST_ASSERT_EQUAL_MEMORY
but with its own perks, let see first out code to test where we declare a simple structure to represent a point in space with there coordinates x and y, plus one function to init the structure with a given values.
app/dummy.h
#ifndef __DUMMY_H__
#define __DUMMY_H__
typedef struct _point
{
uint32_t x;
uint32_t y;
} point_t;
void init_point( point_t *point, uint32_t x, uint32_t y );
#endif // __DUMMY_H__
app/dummy.c
#include <stdint.h>
#include "dummy.h"
void init_point( point_t *point, uint32_t x, uint32_t y )
{
point->x = x;
point->y = y;
}
To verify if the structure was set with values we can use the macro TEST_ASSERT_EQUAL
to test each of the elements, one by one
test/test_dummy.c
void test__init_point__using_elements(void)
{
point_t point;
/*call function under test that make use of global function*/
init_point( &point, 0, 0 );
TEST_ASSERT_EQUAL_MESSAGE( 0, point.x, "x is not zero" );
TEST_ASSERT_EQUAL_MESSAGE( 0, point.y, "y is not zero" );
}
BUT you can also use a reference structure initialized with the values to be set by the function. this is going to work flawlessly because the structure has no padding in it (both elements of 32 bits are perfectly aligned in memory)
void test__init_point__using_memory(void)
{
point_t point;
point_t ref = { 0, 0 };
/*call function under test that make use of global function*/
init_point( &point, 0, 0 );
/*compare memory region of both structures*/
TEST_ASSERT_EQUAL_MEMORY_MESSAGE( &ref, &point, sizeof(point_t), "point is not initialized" );
}
The problem with testing structures
( extracted from Matt Chernosky book: Field Manual for Ceedling, you should super buy his book )
Using a memory comparison to compare two structures is a convenient shortcut. You need to watch out though, because this can cause false errors in some situations. In a memory comparison, it’s just two raw areas of memory being compared — without any regard for what’s stored there. When you create a structure in C, it’s possible to end up with holes when the members don’t align with word size of the machine — and you need to make sure these get initialized correctly. Consider an adc_config_t struct that is composed of an 8-bit value, followed by two 32-bit values.
typedef struct
{
uint8_t channel;
uint32_t clock_rate;
uint32_t sample_rate;
} adc_config_t;
When the compiler constructs this in memory on my 32-bit PC it’s going to use three 32-bit values, which will look something like this:
If I allocate one of these structures on the stack by creating a local function variable, none of the structure memory will be initialized. If I then set the values of all the structure members, this isn’t enough to initialize all of the memory allocated for the structure. The holes remain uninitialized. This means that they could contain any values at all. For example, if I create a struct on the stack and initialize it like this:
void test_function (void)
{
adc_config_t config;
config.channel = 3;
config.clock_rate = 40000000;
config.sample_rate = 1000;
}
Then only the bytes used by these fields are initialized — here is what happens to the memory:
Usually this wouldn’t be a problem — if you’re just accessing a structure by its members. But when we use a memory comparison to compare structures, these uninitialized memory holes can cause false errors. The way to deal with this is to always zero initialize your structures. Here’s a convenient way to do this:
void test_function (void)
{
adc_config_t config = {0};
config.channel = 3;
config.clock_rate = 40000000;
config.sample_rate = 1000;
}