Embedded TDD: Mocking

Posted by Austin Glaser on May 4, 2017 3:07:00 PM
Find me on:

"Mocking" (or "faking") is the process by which a different version of a software module is substituted in a test for that which will be used at runtime. It can be an extremely valuable tool for managing abstraction layers and, especially, for managing hardware dependency.

I've mentioned mocking in passing in several previous posts. This one will deal with the topic in detail. In general, I'll be covering the what, why, and why not of the topic.

I won't spend a great deal of time on the how, since that's highly dependent on the set of tools you'll use. However, I will again call out the Throw The Switch collection, and especially CMock. I use these on a daily basis, and while they have their share of problems I've found them to be better than any other testing tools I've tried for the C language.

Now, enough plugs -- let's get down to business.

 

What?

The term "mock" has some ambiguity. When I use it, however, I mean something fairly specific.

 

Mocks

A mock is a software module which:

  • Implements the interface of another module in the system
  • Is intended only to be used during unit testing
  • Allows unit tests to define expected sequences of function calls, and their arguments
  • Allows unit tests to inject data through the return values of expected functions

Additionally, mocks will almost always be automatically generated rather than manually written -- though I don't consider this a hard and fast property.

 

Fakes

Another term which will come up in this discussion is a "fake." Fakes share some similarity with mocks, but for this discussion there are some important distinctions.

A fake is a software module which:

  • Implements the interface of another module in the system
  • Is intended only to be used during unit testing
  • Maintains some internal state
  • Allows unit tests to examine and directly modify this internal state
  • Modifies this state based on function calls to the public interface

In general, fakes are written manually.

 

An Example

Let's take as an explanatory example a module which, in production, talks to an EEPROM chip. We'll keep it simple -- it'll have two methods, uint8_t eeprom_read(uint32_t address) and void eeprom_write(uint32_t address, uint8_t val).

/* eeprom.h */

uint8_t eeprom_read(uint32_t address);
void eeprom_write(uint32_t address, uint8_t value);

We want to test the increment module, which does some simple and mindless stuff:

/* increment.h */

void increment_eeprom_value(uint32_t address);

/* increment.c */

void increment_eeprom_value(uint32_t address)
{
    uint8_t val = eeprom_read(address);
    eeprom_write(address, val + 1);
}

When testing a module which calls these methods, we can't directly use the eeprom module since no equivalent chip is accessible to our development environment. We can write our test using either a fake or a mock.

In the below examples, I use the syntax valid for CMock and Unity (a test framework). It should be fairly self explanatory, but for clarification you can consult the documentation.

When using a mock, we'd express the exact sequence of reads and writes we expect. Thusly:

/* test_increment.c */

#include "increment.h"
#include "mock_eeprom.h" /* Automatically generated */

void test_increments_a_value(void)
{
    eeprom_read_ExpectAndReturn(0x500, 32);
    eeprom_write_Expect(0x500, 33);

    increment_eeprom_value(0x500);
}

For a simple module like this, it's clean and straightforward. However, as the function under test gets more and more complex, it might not make sense to put expectations on the precise sequence of reads and writes that happen -- after all, what you really care about is the starting and ending state of the EEPROM's memory. So maybe we'll write a fake instead, which stores the EEPROM's state as an array in memory. Here's what it might look like:

/* fake_eeprom.h */

#define FAKE_EEPROM_SIZE 1024

extern uint8_t fake_eeprom_mem[FAKE_EEPROM_SIZE];

void fake_eeprom_reset(void);

/* fake_eeprom.c */

#include <string.h>

uint8_t fake_eeprom_mem[FAKE_EEPROM_SIZE];

void fake_eeprom_reset(void)
{
    memset(fake_eeprom_mem, 0xFF, sizeof(fake_eeprom_mem));
}

uint8_t eeprom_read(uint32_t address)
{
    return fake_eeprom_mem[address];
}

void eeprom_read(uint32_t address, uint8_t val)
{
    fake_eeprom_mem[address] = val;
}

Our test, now, can look like the following:

/* test_increment.c */

#include "increment.h"
#include "fake_eeprom.h"

void test_increments_a_value(void)
{
    fake_eeprom_reset();
    fake_eeprom_mem[0x500] = 32;

    increment_eeprom_value(0x500);

    TEST_ASSERT_EQUAL_UINT8(33, fake_eeprom_mem[0x500]);
}

Of course, real tests and modules will generally be much more complex.

The line between mocks and fakes is not always clear -- in real applications, one tends to blend into the other. Nor is it always easy to determine which strategy should be used in a given situation.

 

Why?

Mocking is the single factor which allows effective and complete unit testing of embedded systems. In short, it allows us to break the hardware dependency of the code we're writing. But it's important to keep in mind when, where, and how to use it. Here's a partial list of some of its powerful advantages:

 

Breaking Hardware Dependency

This is first, because it's arguably the most important. Mocking allows us to simulate the behavior of software modules which, in their native form, access hardware registers, perform IO, or call non-native assembly code. If they were compiled and run directly on the development system, they would either not compile or access protected memory.

 

Injecting errors

Mocking provides a wonderful way to inject hard-to-produce errors into modules-under-test and ensure they're handled properly. Simulating a malfunctioning peripheral bus or a hopefully-unreachable software state would be difficult to do in a reproducible manner on the target hardware, but with a mock or a fake it can be as simple as controlling the return value from the stubbed functions.

 

Managing complexity

Unit tests which must exercise the entire "stack" of a top-level module will quickly get obscenely complex. To manage this, it may make the most sense to test such modules "level-by-level," where each abstraction layer is only tested against its use of the interfaces below it.

However, when you encounter such a situation in your code (especially when the layers of abstraction really start piling up), it may be time to reconsider your overall architecture. Often, it's possible to invert your dependencies, or to "unstack" the various software modules, making them independent by inserting some simplistic and easily tested glue code.

 

Testing glue

"Glue code" is extremely simple code used to join different modules together. As mentioned above, it can often be a worthwhile alternative to having two modules directly call one another. The inherently simplistic nature of glue code makes it amenable to tests which simply ensure that, for instance, the data coming out of one function gets passed into another.

Below is a simplistic example of glue code:

/* sensor.h */

uint16_t sensor_read(void);

/* filter.h */

uint16_t filter_update(uint16_t raw);

/* glue.h */

uint16_t sensor_filtered(void);

/* glue.c */

uint16_t sensor_filtered(void)
{
    return filter_update(sensor_read());
}

/* test_glue.c */

#include "glue.h"
#include "mock_sensor.h"
#include "mock_filter.h"

void test_sensor_filtered(void)
{
    sensor_read_ExpectAndReturn(32);
    filter_update_ExpectAndReturn(32, 54);

    TEST_ASSERT_EQUAL_UINT16(54, sensor_filtered());
}

Of course, in a real project you probably wouldn't call the module glue; it might live in main.c or somewhere else appropriate.

 

Why not?

Despite all the good reasons to use mocks, it's critical to keep in mind that any time a unit test relies on this technique it's making assumptions about the behavior of the mocked module. As these assumptions become more pervasive, your unit tests will become less and less useful.

On the other hand, some level of assumption is always necessary. These are, after all, just unit tests -- their stated purpose is to test code units in isolation from one another. Integration tests, or full-blown functional testing, is critical to ensuring your system works as expected top-to-bottom. But restructuring your code to minimize the necessity of mocking can often increase the efficacy of your unit tests -- while generally decreasing dependencies and thus making re-use easier.

Below is a brief list of possible techniques, each of which could be expanded to a blog post in its own right:

  • Inverting dependencies, moving I/O and hardware dependency to the top layer
  • Breaking dependencies. That is, if module A calls module B, modify the code so that both are self-contained and a thin glue module passes values between them where necessary
  • Eliminating hardware dependency. Writing portable code that doesn't need to be mocked, and can be tested in isolation

 

Conclusion

This post is a whirlwind tour of mocking, faking, and various situations. It's a good reference to keep in the back of your mind as you dive into projects -- and may prove more useful if returned to after you've had hands-on experience with some real-world situations.

 

Interested in learning more about Boulder Engineering Studio? Let's chat!

Schedule A Consult