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.
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 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 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:
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 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 unittest
s 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 unittest
s 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 unittest
s, 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 unittest
s are currently enabled, but we'll save that for the next chapter.
3.149.228.138