Contract programming and unit tests

D has built-in support for contract programming and unit testing. D's contract programming implementation consists of two loosely related features: invariants and function contracts. None of these features would be as useful as they are without the assert expression.

Before we dig into the details, I'd like to point out that all of these features, except unit tests, are enabled by default. Passing -release to the compiler will disable asserts, function contracts, and invariants. Typically, you'll want to leave that flag out during development and use it when you are ready to start testing the release version.

Assert contracts

The assert expression evaluates a Boolean expression and throws an AssertError when the result is false. The basic syntax takes two forms:

assert(10 == 10);
assert(1 > 0, "You've done the impossible!");

Both of these examples will always evaluate to true since the Boolean expressions use constants. If the second one did somehow fail, the text message following the expression would be printed as part of the AssertError message.

When the assert condition can be evaluated to 0 at compile time, the expression is always compiled in, even when -release is enabled. This is useful in code you expect to be unreachable, such as in the default case of a switch statement that covers a limited number of cases. All of the following forms trigger the special behavior:

assert(0);
assert(false);
assert(10 - 10);
assert(1 < 0);

The D reference documentation explicitly says that assert is the most basic contract. As such, it allows the compiler the freedom to assume that the condition is always true and to use that information to optimize any subsequent code, even when asserts are disabled. The rationale is that the assert expression establishes a contract and, since contracts must be satisfied, an assert failure means the program has not satisfied the contract and is in an invalid state. In practical terms, this means that the language allows the compiler to behave as if assert expressions are being used as intended: to catch logic errors. They should never be used to validate user input or test anything that is subject to failure at runtime; that's what exceptions are for.

Function contracts

Function contracts allow a program's state to be verified before and after a function is executed. A contract consists of three parts: in, body, and out. Of the three, only body is required, as it's the actual implementation of the function. For example:

void explicitBody() body { writeln("Explicit body."); }
void implicitBody() { writeln("Implicit body"); }

Every function has a body, but the keyword is not necessary unless either in or out, or both, is also used. The declarations can appear in any order:

enum minBuffer = 256;
size_t getData(ubyte[] buffer)
in {
  assert(buffer.length >= minBuffer);
}
out(result) {
  assert(result > 0);
}
body {
  size_t i;
  while(i < minBuffer)
    buffer[i++] = nextByte();
  return i;
}

This example shows a function, getData, that has both in and out contracts. Before the body of the function is run, the in contract will be executed. Here, the length of buffer is tested to ensure that it is large enough to hold all of the data. If the in contract passes, the body is run. When the function exits normally, the out contract is executed. The syntax out(result) makes the return value accessible inside the out contract as a variable named result (it's worth noting that function parameters can be used in an out contract). This implementation just makes sure the return value is greater than 0.

Invariants

Invariants are added to a struct or class declaration in order to verify that something about the state of an instance, which must be true, is actually true. For example:

class Player {
  enum MaxLevel = 50;
  private int _level;
  int level() { return _level; }
  void levelUp() {
    ++_level;
    if(_level > MaxLevel)
      _level = MaxLevel;
  }
  invariant {
    assert(_level >= 0 && _level <= MaxLevel);
  }
}

In this example, _level cannot be modified directly outside the module; it's a read-only property. It can only be modified through levelUp. By adding an invariant that verifies _level is within the expected range, we guarantee that any accidental modification will be caught. For example, what if we modify the levelUp function and accidentally remove the > maxLevel check, or if we do some work elsewhere in the module and modify _level directly? The invariant is a safeguard.

Invariants are run immediately after a constructor, unless the instance was implicitly constructed with .init, and just before a destructor. They are run in conjunction with function contracts, before and after non-private, non-static functions, in this order:

  • In contract
  • Invariant
  • Function body
  • Invariant
  • Out contract

Something that's easy to overlook is that invariants are not run when a member variable is accessed directly, or through a pointer or reference returned from a member function. If the variable affects the invariant in any way, it should be declared private and access should only be allowed through getter and setter functions, always returning by value or const reference.

Non-private and non-static member functions cannot be called from inside an invariant. Attempting to do so will enter an infinite loop, as the invariant is run twice on each function invocation. Additionally, the invariant can be manually checked at any time by passing a class instance, or the address of a struct instance, to an assert:

auto player = new Player;
assert(player);
assert(&structInstance);

Unit tests

Unit tests are another tool to verify the integrity of a code base. They are implemented in unittest blocks. Any number of unittest blocks can be added at module scope and in class, struct, and union declarations. It is idiomatic to place a unittest block immediately after the function it is testing. Anything valid in a function body can go into them, as they are functions themselves. Here's a simple example:

int addInts(int a, int b) { return a + b; }
unittest {
  assert(addInts(10, 1) == 11);
  assert(addInts(int.max, 1) == int.min);
}

To enable unittests in an executable, pass the -unittest flag to the compiler. This will cause DRuntime to run each unit test when the program is executed after static constructors and before main. All unit tests in a given module are run in lexical order, though the order in which modules are selected for execution is unspecified. However, it is often more convenient to compile and test a single module, rather than an entire program. To facilitate this, DMD provides a switch that will automatically generate a main function if one does not already exist. This creates an executable from a single module that can be used to specifically run the unittests in that module.

Save the previous snippet in a file called utest.d. Then execute the following command:

dmd -unittest --main -g utest.d

Run the resulting binary and you shouldn't see any output. Note –g on the command line. When running unittests, it's always helpful to generate debug info to get the full stack trace. Let's look at that now. Change the 11 to 12 so that the first assert fails. You should get an AssertError with a stack trace pointing to the failure. --main tells the compiler to generate a main function for the module. This is useful to test modules in isolation.

The unittest blocks can be decorated with function attributes, such as nothrow. This really comes in handy when testing template functions, for which the compiler is able to infer attributes. They can also be documented with Ddoc comments. This will cause the code inside the block to become an example in the documentation for the preceding function or type declaration. To prevent this behavior, the unittest can be declared private. There is also a feature available at compile time to determine whether unittests are currently enabled, but we'll save that for the next chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.54.255