Python Unit Testing with SCons – Best Practices

commandline-build-toolpythonunit testing

I am using scons to build a large project containing a mix of C++ and Python. I would like scons to run Python unit tests either using nose or not. Currently, we have a long list of tests files and run a test builder on each one. This causes each test file to be run as a separate script which feel inelegant and inefficient.

Is there a better way of doing this?

Best Answer

In the end, I created a custom builder which called an external test runner. Just save the following to site_scons/site_tools/run_py_test.py, import it in the main SConstruct file, and use it as you would any other builder.

import sys
import os
import subprocess
try:
    from fabulous.color import red, green, fg256
    utest_fail = lambda x: sys.stderr.write(red(x).as_utf8+'\n')
    utest_pass = lambda x: sys.stdout.write(green(x).as_utf8+'\n')
    utest_directory = lambda x: sys.stdout.write(fg256(63, x).as_utf8+'\n')
except ImportError:
    sys.stdout.write('Fabulous module not found: '
            'To install it, run sudo pip-python install fabulous\n')
    utest_fail = lambda x: sys.stderr.write(x)
    utest_pass = lambda x: sys.stdout.write(x)
    utest_directory = lambda x: sys.stdout.write(x+'\n')


def which (program):
    """Reimplementation of the shell command 'which'.

    @see: http://stackoverflow.com/a/377028/232794
    @type program: String
    @param program: The program name.
    @return The full path to the program or None.
    """
    is_exe = lambda x: os.path.isfile(x) and os.access(x, os.X_OK)
    fpath, fname = os.path.split(program)
    if fpath:
        if is_exe(program):
            return program
    else:
        for path in os.environ["PATH"].split(os.pathsep):
            exe_file = os.path.join(path, program)
            if is_exe(exe_file):
                return exe_file
    return None


def runTests (target = None, source = None, env = None):
    """Run a test aggregation tool (by default py.test) in directories 
    provided in TEST_DIR_LIST.

    @type target: String
    @param target: A target.
    @type source: String
    @param source: A source.
    @type env: SCONS ENV.
    @param env: This should contain TEST_RUNNER or TEST_DIR_LIST otherwise
                defaults values are used.
    """
    if not 'TEST_RUNNER' in env:
        env.Append(TEST_RUNNER='py.test')
    if not 'TEST_DIR_LIST' in env:
        sys.stderr.write("+++ TEST_DIR_LIST is not set, using project root.\n")
        env.Replace(TEST_DIR_LIST=['./'])
    cmd = which(env['TEST_RUNNER'])
    if cmd:
        retCode = 0
        for path in env['TEST_DIR_LIST']:
            utest_directory('\n+++ Running unit tests in directory %s:' % (path))
            retCode += subprocess.call([cmd], cwd=path)
        line = "="*80 + "\n"
        if retCode:
            try:
                utest_fail(line + u" ✘  Unit tests failed!\n" + line)
            except UnicodeEncodeError:
                utest_fail(line + " -  Unit tests failed!\n" + line)
        else:
            try:
                utest_pass(line + u" ✓  Unit tests passed.\n" + line)
            except UnicodeEncodeError:
                utest_pass(line + " +  Unit tests passed.\n" + line)
        env.Exit(retCode)
    else:
        sys.stderr.write("*** Could not find %s command\n" % (
            env['TEST_RUNNER']))
        env.Exit(-1)

If you are going to down vote, at least leave a comment as to why this is a bad answer so it can be improved!!!

Related Topic