Unittest

From D Wiki
Jump to: navigation, search

Usage

D has a simple but very useful construct, the unittest block. For example:

real complicatedComputation(real[] args)
{
    real result;
    result = /* perform some fancy computation here */;
    return result;
}

unittest
{
    // Test complicatedComputation by calling it with arguments that
    // produce a known result
    auto knownArguments = [
        1.0, 1.61803, 2.71828
    ];
    auto result = complicatedComputation(knownArguments);

    // Check that the result is what you expected
    enum expectedAnswer = 3.14159;
    enum tolerance = 1e-12;
    assert(abs(result - expectedAnswer) < tolerance);
}

Code in this block is not executed at runtime unless you run DMD with the -unittest command-line option. Programs compiled with the -unittest option will run all unittests in the program first, before the main() function is entered. This allows you to write unittests for your code within the source file itself, thus making it more conducive for encouraging programmers to write unittests.

Advantages

  • You don't have to implement your own unittesting framework, as DMD provides it for free.
  • You don't have to switch over to another unittesting system, and possibly write the testing code in a different language.
  • You can write unittests to check for corner cases immediately after writing your code, while the code is still fresh in your mind, rather than later when you may have forgotten about these corner cases.
  • You can even write unittests first, and then write the code to implement the feature being tested until all unittests pass, after which you know that your code is ready to use.
  • If you make a code change that introduces a bug in previously-working code, your program will refuse to start until the failing unittest is addressed. This reduces regressions in your code.
  • It's so easy to write unittests in D that you should be ashamed to write code without any unittests!

Disadvantages

The primary advantage of D's built-in unittest blocks is their plain simplicity that makes them so easy to write. The disadvantage, however, is that they don't scale very well to more comprehensive tests, and there are some situations where they may not work as well.

  • If you need to stress-test your code, putting the stress test in a unittest block will increase your program's startup time.
  • If there are too many unittest blocks, program startup time will also be slow.
  • Some stress-tests may not be suitable to run prior to program startup, but rather should be run separately from the program itself.
  • There is no simple way of choosing which set of tests to run, except by fiddling with compilation flags to compile some files with unittests and some without. (This still doesn't prevent unittests inside templates from running, though, regardless of whether the module itself was compiled with -unittest.)
  • There is no easy way to skip a failing unittest in an unrelated module that you're not currently working on.
  • If you're writing a library, you may not want your users to suddenly inherit a whole bunch of unittests from your library just because they enabled -unittest to test their own code.

In such situations, you may have to resort to an externally-driven framework to do your unittesting. Even here, however, the built-in unittests can still help: you can use version to include a main function in each source file when compiled with -unittest, the idea being that when unittesting, you will compile each file separately and run the corresponding executables separately.

Placement

There are several considerations of where to place unittest blocks. Some are purely according to taste, some have pragmatic effects.

At the end of the file

One way to avoid cluttering real code with long unittest blocks is to collect all unittests at the end of the source file. This has the advantage of keeping the actual code in one place.

Following the code being tested

Another convention is to place unittests immediately after the function or class that it's testing. This has the advantage of keeping logically-related unittests and code together, so that it's easy to see how much code coverage your unittests have.

Unittests for templated structs and classes

Some special idioms apply when writing unittests for templated structs and classes. For example, if we have a templated struct:

struct MyStruct(T)
{
    T data;
    void processData()
    {
        /* do something with .data */
    }
    // A
}
// B

We can either put the unittest block for processData at the point marked A, or the point marked B.

  • If we put the unittest block at point A, it will be instantiated along with the rest of the struct, for every type T that is used with MyStruct. In other words, the unittest will be run per instantiation of MyStruct.
  • If instead we put the unittest block at point B, outside the scope of MyStruct, then it will only run once.

A good way to decide between the two is to consider what's in the unittest block. If we're testing MyStruct's behaviour for specific values of the template parameter T, then we should put it at B. If we're testing something general that applies to every T, then we should put it at A. For example:

struct MyStruct(T)
{
    T data;
    void processData()
    {
        /* do something with .data */
    }
    unittest // A
    {
        auto ms = MyStruct!T;  // N.B.: parametrized on whatever value of T was used
                               // to instantiate MyStruct
        ms.processData();
        assert(isExpectedResult(ms.data));
    }
}
unittest // B
{
    auto msi = MyStruct!int;  // N.B.: parametrized on specific type we want to test
    msi.processData();
    assert(msi.data == 123);  // Test for expected int result

    auto msr = MyStruct!real; // ditto
    msr.processData();
    assert(abs(msr.data - 2.71828) < 1e12); // Test for expected real result
}

The unittest marked A will be run multiple times, once per instantiation of MyStruct, whereas the unittest marked B will only be run once.

Unittest-specific imports and helper functions

Sometimes your unittest blocks need to use facilities like std.stdio.writeln, which your module itself has no need of. Or you may wish to have a common helper function that all of your unittests can use, but this function is useless outside of unittests.

In such situations, you can use D's version construct to import std.stdio and/or declare your helper functions only when compiling with -unittest:

module MyModule;

import std.algorithm;   // used by real code

version(unittest)
{
    // used only by unittests
    import std.stdio;
    void helperFunction() { ... }
}

// Real module code here:
void ModuleApiFunction()
{
    //writeln(...);   // ERROR: cannot use writeln here when not compiling with -unittest
    //helperFunction(); // ditto
}

unittest
{
    // You can use writeln here
    writeln("Running first unittest!");

    // as well as the helper function
    helperFunction();
}

unittest
{
    // The whole point of putting helperFunction inside version(unittest)
    // instead of just declaring it inside the unittest block is so that
    // you can call it from multiple unittest blocks.
    writeln("Running second unittest!");
    helperFunction();
}