Henry Bergin Henry Bergin - 5 months ago 48
C Question

Unit Testing C With Functions Not in Header

I'm starting to get into unit testing and I'm having trouble understanding something. My struggle boils down to how I would go about testing functions that are only in the .c source, and not declared in the .h header. There's certain functions that shouldn't need to be called outside of the implementation because they're only relevant to that specific file. Since they're not visible to other parts of the program, that means my unit testing cases file can't see those inner functions, therefore making me unable to test them. I've gone around the issue by using forward declarations in the test cases file, but that seems kinda messy and would get to be a pain to go and change if I modify the function parameters.

Are these functions just not meant to be covered by unit testing? I've read that with OOP, you shouldn't be testing private functions because they're tested implicitly through the public functions, but it feels uncomfortable to not have these functions covered (some of which can get quite complex).

Answer Source

Blackbox testing is about testing the software contract between your publicly visible interface and the users. To test this, one typically creates a set of test cases using a tool or a separate test program, that #include's your header file .h which defines your external interfaces. Sounds like you already have this. Great!

What is missing is the concept of White Box testing. This is every bit as important as black box testing for many industries such as telecom, railways, aerospace, or any other industry where high availability and quality need to be assured to a high degree.

For White Box testing, create a separate "private" interface that is used only by your White Box test program. Notice, that in C you can create multiple header files for a given C implementation file. From the compiler's perspective, there is no real enforcement of the number of headers or their names. It is best to stick to a convention as dictated by your project or team.

For our projects, we create a public header (with a simple .h suffix) for our external interfaces, and private header (_pi.h) for our private interfaces intended for a select few who need to access private interfaces such as white box testing, auditing data structures, internal provisioning and diagnostics, and debug tools. Of course, the _pi.h suffix is just a convention, but it works well in practice.

White Box testing is very useful to test your internal functions and data structures, and can go well beyond the limits of Black Box testing. For example, we use White Box test cases to test internal interfaces, and to see what happens when our data structures get corrupted, as well as corner cases such as testing what our code does when passed an unexpected parameter value internally.

For example, let's say we have a file called foo.c that we wanted to perform white box testing on. Then we would create two headers: foo.h and foo_pi.h for external and internal users respectively.

File foo.h

#ifndef FOO_H
#define FOO_H

typedef int FooType;

// Public header for Foo
void Foo(FooType fooVal);
void Bar(void);


File foo_pi.h

// PI should also include the public interface
#include "foo.h"

// Private header for Foo
// Called by White Box test tool 
void FooBar_Test1(FooType fooVal);
void Foo_Internal(void);
void Bar_Internal(void);


File foo.c

#include "foo.h"
#include "foo_pi.h"
// Notice you need to include both headers

// Define internal helpers here
static FooType myFooVal = 0; 
void FooBar_Test1(FooType fooVal) {myFooVal = fooVal;}

void Foo_Internal() {Bar_Internal();}
void Bar_Internal(void) {myFooVal++;}      

// Define external interfaces after the helpers
void Foo(FooType fooVal) {myFooVal = fooVal; Foo_Internal();}
void Bar(void)           {Bar_Internal();}

// Main() not typically included 
// if this is just one module of a bigger project!
int main(int argc, char** argv)

If you are confused what all those #ifndef/#define/#endif things are, these are CPP macros, and this use is not enforced in C but it is a widely used convention. For more details, see https://stackoverflow.com/a/42744341/6693299

While I did not show it in the example above, the Foobar_test() routine (and any other internal test methods, would typically be placed in a separate module reserved for internal test functions. Then they can be packaged out of the final product. Along with some fancy CPP preprocessing that I won't describe here, you can conditionally-compile out the private headers and the test functions, and make the interface secure for the production load (after White Box testing is done). But that is probably too much detail!

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download