In collaboration with Jeff Hui
Let’s say component A depended on component B (an adapter to a third party library). The goal is to provide a way to assert the correct behavior of component A as it depends on B — Was a function called? Was a function called with the right arguments? Did the function produce the right output? Etc. — What is it about C that makes testing these behaviors more challenging than say in Ruby or Objective-C?
C is a statically typed language and does not provide virtual functions or dynamic dispatching. In other words, unlike Ruby or Objective-C, we can’t replace at runtime any of our functions for fakes. But how would we replicate a fake in C-land?
Let’s Drive some ‘C’
One way would be to create a fake implementation of your C functions and have them store state into global variables. Then under specs, you would swap out the real implementation for its fake at C’s linker phase and assert the stored values through public getters.
Let’s see what this would look like. We want to drive out the implementation of Subject.c, the ‘subject’ under test. Let’s assume that in the process we discover it needs to make a http request using a third party library so we’ll create an adapter for the library – Adapter.h. In response, we’ll also need to create a fake version of Adapter.c that implements every function of our real adapter.
Adapter.h
// Adapter.h
void requestWrapper();
FakeAdapter.h and FakeAdapter.c
// FakeAdapter.h
#include 'Adapter.h'
void resetFake();
bool getWasRequestWrapperCalled();
// FakeAdapter.c
#include 'FakeAdapter.h'
bool wasRequestWrapperCalled = false;
void resetFake()
{
wasRequestWrapperCalled = false;
}
void requestWrapper()
{
wasRequestWrapperCalled = true;
}
bool getWasRequestWrapperCalled()
{
return wasRequestWrapperCalled;
}
What did we do here? We have our public interface for Adapter.h and we wanted to make sure our FakeAdapter implemented each function. We included Adapter.h inside our FakeAdapter’s header so that we wouldn’t have to copy/paste each function declaration from Adapter.h. We also added any getters/setters to our fake’s interface for our tests to use as well as a means of reseting our fake.
To use our fake, try the following:
- You could write a make file to specify which files should be included at compile time or if you are using Xcode, under Target Membership, you can choose what files should be included when you build a target for compilation.
- The fake implementation (FakeAdapter.c) needs to be included in the specs target and not your App target, such that when you build/run your specs target, your test will use your fake adapter’s implementation instead of the real adapter’s implementation. Symmetrically, the real implementation (Adapter.c) should only be included in your App target. You can do this by selecting FakeAdapter.c and checking your Specs target in the Target Membership panel:
- Now when you run the Specs target, it will have had access to your fake’s implementation which stored any state for you to assert on in your test.
Here’s an example of what our SubjectSpec.mm could look like if we were using Cedar as our testing framework to produce a failing test:
SubjectSpec.mm
// SubjectSpec.mm
#include 'FakeAdapter.h'
describe("when it makes a request", ^{
beforeEach(^{
resetFake();
makeRequest(); // drive out this implementation
});
it("should use the wrapper API", ^{
getWasRequestWrapperCalled() should be_truthy;
});
...
});
Why does this work?
We take advantage of C’s build phases, specifically compilation and linking.
- At compilation, the compiler builds an object file from a (.c) file and embeds any (.h) file contents.
- The linker phase is where our magic happens. The linker assigns the final addresses to functions declared in the obj file among doing other things to output an executable file.
It’s at the linker phase that the spec in our example used the fake implementation instead of the real implementation. When the linker tried to find the implementation for requestWrapper() from adapter.h, it assigned our fake’s requestWrapper() instead of our real one.
Pros/Cons
This is just one example of testing C code and like most things, there are trade-offs:
- Pro: We are using the compiler and linker to our advantage.
- Con: How would we test our dependencies (Adapter.c) using this technique?
Some Alternatives
In a future post, I’ll be discussing a couple more methods for testing C:
– Using structs to store extra information for testing
– #ifdef macros
– – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – – –
Credit: Big thanks to Jeff Hui for explaining the technique and for providing input into writing this blog post