unit testing in SCons

unit testing in SCons

The scons-users mailing list has a few references to how people have implemented unit tests in their build, very few of those are accompanied by code examples. The SCons wiki has a page on unit tests, which sucks to say the least.

What I want is what Greg Ward wants:

  • all the tests should run when one types scons check.

  • if SCons builds nothing, the unit tests should also not run

  • if the unit test source changes, the unit test should be built and run

  • if code that the test links or includes changes, the test should be rebuilt and run

The second part is pretty easy, actually. SCons’ dependency handling is so awesome it just works. You can ensure that a program is run after it is built by using AddPostAction:

1
2
test = env.Program('test.c')
env.AddPostAction(test, test[0].abspath)

and as long as your test program is self contained (i.e. it’s a real unit test and not a subsystem test framework, i.e. testing various inputs as they go through your parser, say) then it just works.

You can build the first rule with an Alias – SCons will let you append targets to an Alias to accumulate what that alias does:

1
env.Alias('check', test)

and you can make sure that the test is always “run” (i.e. built) by using AlwaysBuild:

1
env.AlwaysBuild(test)

(In the latest version of SCons, the soon to be 0.97 that is currently still in CVS, you can call AlwaysBuild on an alias, making this last line look instead like:

1
env.AlwaysBuild('check')

and then you only need it once.)

The problem with this approach is that you really want to alias the post action, not the build of the test. Consider this:

% scons check
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
gcc -o input/filtergen/t/scan input/filtergen/t/scan.o
/home/jaq/src/filtergen/filtergen--unittests/input/filtergen/t/scan
scons: done building targets.

Notice how scan is relinked before execution? That’s because we told SCons to AlwaysBuild the test program. But when we’re running the test, we only want to relink the program iff its source or dependencies have changed. I haven’t yet found a way to make this work the “right” way – I’ve tried to use Command in place of the post action with no success.

Anyway, you can bundle this all together with a new Tool like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from SCons.Script.SConscript import SConsEnvironment

def UnitTest(env, source, **kwargs):
    test = env.Program(source, **kwargs)
    env.AddPostAction(test, test[0].abspath)
    env.Alias('check', test)
    env.AlwaysBuild(test)
    return test

SConsEnvironment.UnitTest = UnitTest

Then use it in your code like so:

1
2
3
4
5
scan = env.UnitTest('scan.c',
                    CPPPATH=testcpppath,
                    LIBPATH=testlibpath,
                    LIBS=testlibs,
                    CPPFLAGS=testcppflags)