#include - there is a third way

Isolating legacy code from external dependencies can be awkward. Code naturally resists being isolated if it isn't designed to be isolatable. In C and C++ the transitive nature of #includes is the most obvious and direct reflection of the high-coupling such code exhibits. There is a technique that you can use to isolate a source file by cutting all it's #includes. It relies on a little known third way of writing a #include. From the C standard:

6.10.2 Source file inclusion
...
A preprocessing directive of the form:
  #include pp-tokens 
(that does not match one of the two previous forms) is permitted. The preprocessing tokens after include in the directive are processed just as in normal text. ... The directive resulting after all replacements shall match one of the two previous forms.


An example. Suppose you have a legacy source file that you want to write some unit tests for. For example:
/*  legacy.c  */
#include "wibble.h"
#include <stdio.h>

int legacy(void)
{
    ...
    info = external_dependency(stdout);
    ...
}


First create a file called nothing.h as follows:
/* nothing! */
nothing.h is a file containing nothing and is an example of the Null Object Pattern). Then refactor legacy.c to this:
/* legacy.c */
#if defined(UNIT_TEST)
#  define LOCAL(header) "nothing.h"
#  define SYSTEM(header) "nothing.h"
#else
#  define LOCAL(header) #header
#  define SYSTEM(header) <header>
#endif

#include LOCAL(wibble.h)  /* <--- */
#include SYSTEM(stdio.h)  /* <--- */

int legacy(void)
{
    ...
    info = external_dependency(stdout);
    ...
}


Now structure your unit-tests for legacy.c as follows:
First you write the fake implementations of the external dependencies. Note that the type of stdout is not FILE*.
/* legacy.test.c: Part 1 */

int stdout;

int external_dependency(int stream)
{   
    ...
    return 42;
}
Then #include the source file. Note carefully that we're #including legacy.c here and not legacy.h
/* legacy.test.c: Part 2 */
#include "legacy.c" 
Then write your tests:
/* legacy.test.c: Part 3 */

#include <assert.h>

void first_unit_test_for_legacy(void)
{
    ...
    assert(legacy() == expected);
    ...
}

int main(void)
{
    first_unit_test_for_legacy();
    return 0;
}


Then compile legacy.test.c with the -D UNIT_TEST option.

This is pretty brutal, but it might just allow you to create an initial seam which you can then gradually prise open. If nothing else it provides a way to create characterisation tests to familiarize yourself with legacy code.

The -include compiler option might also prove useful.

-include file
    Process file as if #include "file" appeared as the first line of the primary source file.


Using this you can create the following file:
/* include_seam.h */
#ifndef INCLUDE_SEAM
#define INCLUDE_SEAM

#if defined(UNIT_TEST)
#  define LOCAL(header) "nothing.h"
#  define SYSTEM(header) "nothing.h"
#else
#  define LOCAL(header) #header
#  define SYSTEM(header) <header>
#endif

#endif

and then compile with the -include include_seam.h option.

No comments:

Post a Comment