We define unit testing as the test for a single function, the most atomic part of a complete program or from a single software component, it means the test we perform shall be focus only on testing one single function. Then what happens when we decide to test a functionality involving more than one function, well this is what we called integration testing
Let say for instance we have the following piece of code
app/queue.h
#ifndef __QUEUE_H__
#define __QUEUE_H__
typedef struct
{
void *Buffer;
uint8 Size;
uint32 Elements
uint8 Empty;
uint8 head;
uint8 tail;
...
} Queue_config;
void Queue_Init( Queue_config *ConfigPtr, uint32_t Elements, void *Buffer, uint8_t Size )
uint8_t Queue_Write( Queue_config *ConfigPtr, void *Data )
uint8_t Queue_Read( Queue_config *ConfigPtr, void *Data )
...
#endif
app/queue.c
#include "queue.h"
void Queue_Init( Queue_config *ConfigPtr, uint32_t Elements, void *Buffer, uint8_t Size )
{
Queue_config->Buffer = Buffer;
Queue_config->Elements = Elements;
Queue_config->Size = Size;
Queue_config->Empty = TRUE;
...
}
uint8_t Queue_Write( Queue_config *ConfigPtr, void *Data )
{
if( Queue_config->Full != TRUE )
{
Queue_config->Buffer[ Queue_config->Head ] = Data;
...
}
return ..
}
We can easily test the first function, in the following way where we check if the values passed by parameter set correctly in the control structure that we also pass as parameter.
test/test_queue.c
#include "unity.h"
#include "queue.h"
void setUp(void)
{
}
void tearDown(void)
{
}
void test__Queue_Init__paramter_setting_right( void )
{
/*create a queue to test and its corresponding buffer made of 4 bytes and 4 elements*/
Queue_config theQueue;
uint32_t testBuffer[4];
Queue_Init( &theQueue, 4, testBuffer, sizeof(uint32_t) );
TEST_ASSERT_EQUAL_MESSAGE( 4, theQueue.Elements, "the queue has not 4 elemnts as supposed to be" );
TEST_ASSERT_EQUAL_MESSAGE( 4, theQueue.Size, "the size of queue elemnts is not 4 bytes as supposed to be" );
TEST_ASSERT_EQUAL_PTR_MESSAGE( testBuffer, theQueue.Buffer, "the queue has not set the same buffer as supposed to be" );
}
...
And what about the second function Queue_Write
, well this function needs a queue already initialized in order to write a proper element into it. We have two option m,manually modify the control structure or to call Queue_Init
in the same test case, I personally do not like the first option because its clumsy and prone to errors, so let’s take the second one
#include "unity.h"
#include "queue.h"
...
void test__Queue_Write__write_one_single_element( void )
{
/*create a queue to test and its corresponding buffer made of 4 bytes and 4 elements*/
Queue_config theQueue;
uint32_t testBuffer[4];
/*value to write into the queue*/
uint32_t data = 1234;
Queue_Init( &theQueue, 4, testBuffer, sizeof(uint32_t) );
/*this is the actual funtion under test for this test case*/
Queue_Write( &theQueue, &data );
TEST_ASSERT_EQUAL_MESSAGE( data, theQueue.Buffer[ 0 ], "data value was not written in the queue correctly" );
}
...
OK, you can say this is not a unit test because we should only call the function under test in the actual test case, and maybe you are right, but for several reason we prefer this method of testing because it is actually more simple. Another to avoid this can of testing is that you don’t know which function is actually failed in case of error. But this can be easily solve testing the Queue_Init
function first, since Ceedling always run the test cases in order.
setUp function
Maybe you need some initial steps before run the actual tests, like the example before where we call the Queue_Init
function first then the one we want to test, we could avoid this situation using the calling the init function in the setUp
function and using global variables.
static Queue_config gQueue;
static uint32 gBuffer[4];
void setUp( void )
{
Queue_Init( &gQueue, 4, &gBuffer, sizeof(uint32) );
}
void test__Queue_Init__paramter_setting_right( void )
{
/*create a queue to test and its corresponding buffer made of 4 bytes and 4 elements*/
Queue_config theQueue;
uint32_t testBuffer[4];
Queue_Init( &theQueue, 4, testBuffer, sizeof(uint32_t) );
TEST_ASSERT_EQUAL_MESSAGE( 4, theQueue.Elements, "the queue has not 4 elemnts as supposed to be" );
TEST_ASSERT_EQUAL_MESSAGE( 4, theQueue.Size, "the size of queue elemnts is not 4 bytes as supposed to be" );
TEST_ASSERT_EQUAL_PTR_MESSAGE( testBuffer, theQueue.Buffer, "the queue has not set the same buffer as supposed to be" );
}
void test__Queue_Write__write_first_element( void )
{
/*value to write into the queue*/
uint32_t data = 1234;
Queue_Write( &gQueue, &data );
TEST_ASSERT_EQUAL_MESSAGE( data, gQueue.Buffer[ 0 ], "data value was not written in the queue correctly" );
}
Now the test case with write function has only one single function call and is for all means a proper unit test, we previously testing the Init function also with its won corresponding parameters, so it is valid to call again the function during the test setUp. By the way the function setUp is called before every test case, this does not create a conflict with test__Queue_Init__paramter_setting_right
because we are using different variables
Some times is not possible to call one single function under test making an integration test the way to go instead the unit testing, let see some examples:
Example 1:
You want to test when the queue is full, rather than modify manually the internals of our buffer queues and its flags we can call the write operation several times and the test the full flag. Another example of how implement Integration testing
void test__Queue_Write__write_last_element( void )
{
/*fill the queue by writting four elements*/
Queue_Write( &gQueue, &data );
Queue_Write( &gQueue, &data );
Queue_Write( &gQueue, &data );
Queue_Write( &gQueue, &data );
TEST_ASSERT_EQUAL_MESSAGE( gQueue_config.Full, TRUE. "the internal flag Full is not set" );
}
Example 2:
Another example, here we are testing the queue is correctly emptied after writing several elements, for this test the function under test is Queue_Flush
but in this case we need to call another function before, making and actual integration test.
void test__Queue_Flush__removing_two_elements( void )
{
/*fill the queue by writting four elements*/
Queue_Write( &gQueue, &data );
Queue_Write( &gQueue, &data );
Queue_Flush( &gQueue );
TEST_ASSERT_EQUAL_MESSAGE( gQueue_config.Full, FALSE. "the interbnal flag Full is not clear" );
TEST_ASSERT_EQUAL_MESSAGE( gQueue_config.Empty, TRUE. "the interbnal flag Empty is not set" );
...
}
In combination with mocks you can make really some complex integration testing, it will depends on how deep you want to test your function or entire drivers, well you can even combine more than one piece of code to perform integration testing. My advice is to keep it simple as possible. Ans really important the order of your test, for instance from the examples above, it is necessary to test first the Queue_Init
and for the last example code it is important to test first in solitaire the Queue_Write
before Queue_Flush