Thursday, September 12, 2019

Unit-testing static functions in C

An annoying thing about C code is that there are plenty of functions that cannot be unit-tested by some external framework - specifically anything declared as static. Any larger code-base will end up with hundreds of those functions, many of which are short and reasonably self-contained but complex enough to not trust them by looks only. But since they're static I can't access them from the outside (and "outside" is defined as "not in the same file" here).

The approach I've chosen in the past is to move the more hairy ones into separate files or at least declare them normally. That works but is annoying for some cases, especially those that really only get called once. In case you're wondering whether you have at least one such function in your source tree: yes, the bit that parses your commandline arguments is almost certainly complicated and not tested.

Anyway, this week I've finally found the right combination of hacks to make testing static functions easy, and it's:

  • #include the source file in your test code.
  • Mock any helper functions you'd need to trick the called functions
  • Instruct the linker to ignore unresolved symbols
And boom, you can write test cases to only test a single file within your source tree. And without any modifications to the source code itself.

A more detailed writeup is available in this github repo.

For the impatient, the meson snippet for a fictional source file example.c would look like this:

test('test-example',
     executable('test-example',
                'example.c', 'test-example.c',
                dependencies: [dep_ext_library],
                link_args: ['-Wl,--unresolved-symbols=ignore-all',
                            '-Wl,-zmuldefs',
                            '-no-pie'],
                install: false),
)

There is no restriction on which test suite you can use. I've started adding a few of test cases based on this approach to libinput and so far it's working well. If you have a better approach or improvements, I'm all ears.

5 comments:

Cyberax said...

My trick: #define static

Peter Hutterer said...

yeah, I've used that in the past and it's ... unsatisfactory. it doesn't work, messes with your static variables and is actually quite tricky to maintain long-term.

Nacho said...

... or you can use a very expensive tool like VectorCAST or LDRA that does all this automatically.

I really don't see a big point of unit testing static functions. white box testing is only usually useful to reach full code coverage, as mandated for safety-critical applications, but for other uses, black box testing is preferable, testing all possible equivalence classes, and taking into account that you probably have to mock some of the functions (OS or other component interfaces, or hardware accessing functions) to simulate some inputs.

Nacho said...

Another useful trick, for faking function calls, is using the ld's --wrap option.

Peter Hutterer said...

IMO as soon as you have a string parser in a function somewhere, you need unit tests. black box testing isn't always useful or even possible here. same with e.g. commandline arguments.