Difference between revisions of "Unittest"

From D Wiki
Jump to: navigation, search
(Placement)
(Unittests for templated structs and classes: better example)
Line 94: Line 94:
 
     auto msi = MyStruct!int;  // N.B.: parametrized on specific type we want to test
 
     auto msi = MyStruct!int;  // N.B.: parametrized on specific type we want to test
 
     msi.processData();
 
     msi.processData();
     assert(isExpectedIntResult(msi.data));
+
     assert(msi.data == 123); // Test for expected int result
  
 
     auto msr = MyStruct!real; // ditto
 
     auto msr = MyStruct!real; // ditto
 
     msr.processData();
 
     msr.processData();
     assert(isExpectedRealResult(msr.data));
+
     assert(abs(msr.data - 2.71828) < 1e12); // Test for expected real result
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
The unittest marked A will be run multiple times, once per instantiation of MyStruct, whereas the unittest marked B will only be run once.
 
The unittest marked A will be run multiple times, once per instantiation of MyStruct, whereas the unittest marked B will only be run once.

Revision as of 22:11, 14 December 2012

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 while your 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 passed, 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!

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.