I’m going to bend the example a little, and look at how to write tests that interact with an interrupt service routine and again with a semaphore. I’ll also get some help with the tedious parts of building fakes by using Mike Long’s Fake Function Framework. (BTW: I am using a beta version, that has changes the released version does not yet have.)
This figure shows the concurrent runtime relationship between the MessageProcessor
and its SerialDriver
.
The MessageProcessor
tells the SerialDriver
that is wants to wait for some characters, also passing along the Semaphore
it will wait on. MessageProcessor
then waits on its Semaphore
. The ISR collects characters one at a time until it has satisfied its request. Then wakes up the MessageProcessor
by posting to its Semaphore
.
The MessageProcessor
is oblivious to what happens concurrently. It just wants some characters and the request is satisfied asynchronously. Here is the MessageProcessor
code that interacts with the ISR and the RTOS.
int MessageProcessor_WaitForMessage(char * buffer, size_t length) { INT8U error = 0; SerialInterrupt_WaitForString(buffer, length, int_sync); OSSemPend(int_sync, 1000, &error); return error == OS_ERR_NONE; } |
Like in the last article, The OSSempend
test-double satisfies what the caller is waiting for. This picture helps to visualize the faked concurrency.
In addition to simulating that MessageProcessor
is given a message, the test should check that all the right parameters are passed to SerialDriver
and OSSemPend
. We will also need tests for the timeout case.
Instead of hand crafting the test-double like the last two articles, we can generate the boring parts of the test-double with the Fake Function Framework (fff). To use it, we need an fff file. Here is SerialInterruptFake.fff
:
#include "SerialInterrupt.h" #include "fff2.h" FAKE_VALUE_FUNCTION(int, SerialInterrupt_WaitingFor, char *, int, OS_EVENT *); |
An fff file is like a header file but without include guards. I use the file extension fff so I can tell an fff file from an h file. The macro expansion for FAKE_VALUE_FUNCTION
declares all the boiler plate struct
s and functions needed to build a decent test-double. The parameters of FAKE_VALUE_FUNCTION
exactly match the signature of the function you are faking. Here is the signature for SerialInterrupt_WaitingFor
:
int SerialInterrupt_WaitingFor(char *, int, OS_EVENT *); |
The fff file is included in test case files so the fake can be setup and checked. The fff file is also included in a .c or .cpp file to generate the implementation of the test-double. Here is SerialInterruptFake.c
:
#include "SerialInterruptFake.fff" #define GENERATE_FAKES #include "SerialInterruptFake.fff" |
Here is a snip of uCosII_TestDouble.fff
for the uCos-II RTOS fake:
#include "fff2.h" #include "ucos_ii.h" //snip... FAKE_VALUE_FUNCTION(OS_EVENT *, OSSemCreate, INT16U) FAKE_VALUE_FUNCTION(OS_EVENT *, OSSemDel, OS_EVENT *,\ INT8U,\ INT8U *) FAKE_VOID_FUNCTION(OSSemPend, OS_EVENT *,\ INT32U,\ INT8U *) //snip... |
By the way, if you need a fake for some 3rd party code (like an RTOS), you only need to define fakes for the functions that you use.
Let’s look at an all inclusive test, and then we’ll refactor it to make it read better.
static const char * fakeInput; static char buffer[100]; void Successful_OSSemPend(OS_EVENT *event, INT32U timeout, INT8U *error) { memcpy(buffer, fakeInput, strlen(fakeInput)); *error = OS_ERR_NONE; } TEST(MessageProcessor, WaitForMessage_succeeds) { OSSemPend_reset(); SerialInterrupt_WaitingFor_reset(); OSSemPend_fake.custom_fake = Successful_OSSemPend; fakeInput = "sched lightOn 5 Monday 20:00"; CHECK(MessageProcessor_WaitForMessage( (char*)&buffer, sizeof(buffer))); LONGS_EQUAL(1, SerialInterrupt_WaitingFor_fake.call_count); POINTERS_EQUAL(&buffer, SerialInterrupt_WaitingFor_fake.arg0_val); LONGS_EQUAL(sizeof(buffer), SerialInterrupt_WaitingFor_fake.arg1_val); LONGS_EQUAL(1, OSSemPend_fake.call_count); POINTERS_EQUAL(SerialInterrupt_WaitingFor_fake.arg2_val, OSSemPend_fake.arg0_val); STRCMP_EQUAL(fakeInput, buffer); } |
Look that test over carefully. The functions OSSemPend_reset
and SerialInterrupt_WaitingFor_reset
as well as the data structures SerialInterrupt_WaitingFor_fake
and OSSemPend_fake
were generated by fff.
The good news is that the tedium of creating fakes goes ways, but it makes for tests that don’t tell their story so well. No worries, it can be refactored onto a nice clean test.
TEST(MessageProcessor, WaitForMessage_succeeds) { fakeInput = "sched lightOn 5 Monday 20:00"; result = MessageProcessor_WaitForMessage(buffer, length); CHECK_TRUE(result); CheckSerialInterruptCalledWith(buffer, length); CheckOSSPendCall(); CheckBufferContains(fakeInput); } |
With the test refactored, let’s look at the TEST_GROUP
and other elements needed.
extern "C" { #include "MessageProcessor.h" #include "SerialInterruptFake.fff" #include "uCosII_TestDouble.fff" } #include "CppUTest/TestHarness.h" static const char * fakeInput; static char buf[100]; void Successful_OSSemPend(OS_EVENT *event, INT32U timeout, INT8U *error) { memcpy(buf, fakeInput, strlen(fakeInput)); *error = OS_ERR_NONE; } TEST_GROUP(MessageProcessor) { int result; size_t length; char * buffer; void setup() { OSSemCreate_reset(); OSSemDel_reset(); OSSemPend_reset(); OSSemPend_fake.custom_fake = Successful_OSSemPend; SerialInterrupt_WaitingFor_reset(); length = sizeof(buffer); buffer = (char*)&buf; MessageProcessor_Create(); } void teardown() { MessageProcessor_Destroy(); } void CheckSerialInterruptCalledWith(char * buffer, int length) { LONGS_EQUAL(1, SerialInterrupt_WaitingFor_fake.call_count); POINTERS_EQUAL(buffer, SerialInterrupt_WaitingFor_fake.arg0_val); LONGS_EQUAL(length, SerialInterrupt_WaitingFor_fake.arg1_val); } void CheckOSSPendCall() { LONGS_EQUAL(1, OSSemPend_fake.call_count); POINTERS_EQUAL(SerialInterrupt_WaitingFor_fake.arg2_val, OSSemPend_fake.arg0_val); } void CheckBufferContains(const char * expected) { STRCMP_EQUAL(expected, buffer); } }; |
setup()
is pretty straightforward, except maybe the installation of Successful_OSSemPend
into the custom_fake
member of OSSemPend_fake
. When a test provides a custom_fake
, the generated fake calls it after collecting the arguments.
Notice that there is no custom_fake
for OSSemCreate
, OSSemDel
, and SerialInterrupt_WaitingFor
. They do not need custom_fake
implementations. OSSemPend
needs the custom_fake
to do the pseudo concurrent activities.
CheckSerialInterruptCalledWith
makes sure that OSSemPend
is only called once. It also checks each of the passed arguments that were captured in arg0_val
and arg1_val
.
The CheckOSSPendCall
test helper is kind of interesting. To check its arguments we need access to a hidden part of the MessageProcessor
. Not shown in this article is that MessageProcessor
creates a Semaphore
during its creation. The Semaphore
‘s data structure is returned as an OS_EVENT
pointer. Even though the pointer is hidden in MessageProcessor
it is passed to both SerialDriver
and OSSemPend
. The test helper makes sure the passed pointers are equal. If we were really worried about it, the fake for OSSemCreate
could be customized to pass a pointer that we could check.
This next test simulates a timeout condition from the SerialDriver
. We need a different custom_fake
to get the right error code to be returned by OSSemPend
in error
.
void TimeOut_OSSemPend(OS_EVENT *event, INT32U timeout, INT8U *error) { *error = OS_ERR_TIMEOUT; } TEST(MessageProcessor, WaitForMessage_timeout) { OSSemPend_fake.custom_fake = TimeOut_OSSemPend; strncpy(buffer, "some leftover garbage", length); result = MessageProcessor_WaitForMessage(buffer, length); CHECK_FALSE(result); CheckBufferContains(""); } |
Had OSSemPend
returned its result as a return value instead of through *error
, we could have provided the return value through OSSemPend_fake
‘s return_val
or return_val_seq[]
members. Then a custom_fake
would not be needed.
I pruned the above test to only include the essential differences from the prior test. I made sure the buffer contains some garbage, that we get a FALSE
result and an empty string. There is no need to re-check the parameters passed to MessageProcessor
‘s collaborators.
That test drove me to add the error handling to MessageProcessor
like this:
int MessageProcessor_WaitForMessage(char * buffer, size_t length) { INT8U error = 0; SerialInterrupt_WaitingFor(buffer, length, int_sync); OSSemPend(int_sync, 1000, &error); if (error == OS_ERR_NONE) return TRUE; buffer[0] = 0; return FALSE; } |
I’m treating all OSSemPend
errors the same, but if the clients of MessageProcessor_WaitForMessage
needed to know about the specific error, you can see that we could cause other OSSemPend
errors easily. I would resist letting the uCos-II error codes work their way higher in the application, but now I am off on a design tangent.
You can see an RTOS can be stubbed out with no problem with fff and test can be written for code that is concurrency aware. It took me about an hour the edit the Micrium uCos-II header file into submission. The I was off and writing tests for my concurrent code that interacts with the RTOS. I hope you saw the importance of test case refactoring. It will help with future maintenance and increase the documentation value of the tests.
If you want the code that goes with this example, register at my web site. Use course code BLOG-READER. Then look at Registered User Extras.
Excellent!
Pingback: Recent Readings Week 30 | Larry's Notebook
Pingback: Designing and testing embedded systems | jonaslinde.se
Pingback: Japanese article on C, TDD, and Fake Function Framework (fff) at Futurismo
Excellent series of blog posts, but I can’t seem to access the source code. I registered via the link provided and found the resource page, but when I click on the download link, I get a 404 error.
Darn, those files are from my old website. I’ll look for them.