Built-In unittest Blocks
D bakes unit testing into the language syntax itself: unittest { ... } blocks can appear right next to the function they test, are compiled in only when built with -unittest, as 'dub test' does automatically, and are stripped entirely from release builds so there's zero runtime cost in production; this keeps tests physically colocated with the code they exercise instead of living in a separate directory tree.
Cricket analogy: A batter's technique gets checked in the nets right next to the main pitch before a Test match, not in some separate facility across town; D's unittest blocks sit right next to the function they test in the same file, not in a separate test directory.
int add(int a, int b) {
return a + b;
}
unittest {
assert(add(2, 3) == 5);
assert(add(-1, 1) == 0);
assert(add(0, 0) == 0);
}
// Run with: dub test
// or: dmd -unittest -run myfile.dContract Programming: in, out, and invariant
D supports Design by Contract natively: a function can declare 'in' blocks that check preconditions on entry and 'out' blocks that check postconditions, including the return value via out(result), and a class or struct can declare an invariant block that D automatically checks before and after every public method call; like unittest blocks, contracts compile away in release builds by default, letting you write assertions liberally during development without permanent overhead.
Cricket analogy: The third umpire checks the bowler's front foot for a no-ball before the delivery counts, and separately checks whether the ball carried cleanly for a catch after the fact; D's 'in' contract checks input before a function runs and 'out' checks the result after, exactly like those before-and-after umpire checks.
struct Stack(T) {
private T[] data;
void push(T item)
in (data.length < 1000, "Stack overflow: capacity exceeded")
{
data ~= item;
}
T pop()
in (data.length > 0, "Cannot pop from an empty stack")
out (result; data.length >= 0)
{
auto top = data[$ - 1];
data.length -= 1;
return top;
}
invariant {
assert(data.length <= 1000);
}
}Contracts and unittest blocks are both stripped from -release builds by default, which is why D also has plain assert() for checks you might want to keep, and enforce() from std.exception, which throws a catchable exception rather than an assertion failure and remains active in release builds -- use enforce() for input validation that must survive into production.
Assertions and Exception-Based Testing Helpers
Inside unittest blocks, assert(condition) is the workhorse for equality and boolean checks, but std.exception adds sharper tools: assertThrown!Exception(expr) verifies that evaluating an expression throws a specific exception type, assertNotThrown(expr) verifies it doesn't, and std.math's isClose() is the standard way to compare floating-point results without falling into exact-equality bugs caused by rounding.
Cricket analogy: Hawk-Eye doesn't just say 'out' or 'not out' -- it shows the exact ball-tracking path so umpires can verify the specific reason; assertThrown!Exception in D doesn't just check that something failed, it verifies the exact exception type thrown.
Never compare floating-point results with == in a unittest -- rounding differences between compiler optimization levels or CPU instruction sets, such as fused-multiply-add, can make mathematically-equal-looking math produce a different last bit. Use std.math.isClose(a, b, tolerance) instead, or you'll get flaky tests that pass on your machine and fail in CI.
Testing D Code in Continuous Integration
Because unittest blocks compile away by default, CI pipelines must explicitly opt in: 'dub test' builds with -unittest and -g and executes the resulting binary, running every discovered unittest block and reporting the first failing assertion's file and line; for GitHub Actions or GitLab CI, a job typically installs a D compiler via a setup action, runs 'dub test', and can pair it with 'dub test -b unittest-cov' to fail the build if coverage on critical modules drops below a threshold.
Cricket analogy: A ground's DRS system only activates once a captain formally reviews with the signal -- it won't run automatically on every ball; D's unittest blocks only run when the build formally opts in with 'dub test' or -unittest, not automatically on every regular build.
- unittest blocks are a native D language feature, colocated with the code they test, and compiled only with -unittest / 'dub test'.
- in and out contracts check function preconditions and postconditions automatically and are stripped from -release builds by default.
- invariant blocks on a struct/class are checked automatically around every public method call.
- assert() is for internal invariants; std.exception's enforce() throws a catchable exception that survives into release builds.
- std.exception.assertThrown!T verifies a specific exception type was thrown, not just that some error occurred.
- Never compare floating-point results with == in tests -- use std.math.isClose() with an explicit tolerance.
- CI pipelines must explicitly run 'dub test', or build with -unittest, since unit tests do not run in a normal 'dub build'.
Practice what you learned
1. Where are D unittest blocks typically written?
2. What happens to unittest and default contract (in/out/invariant) blocks in a -release build?
3. Why should you avoid comparing floating-point numbers with == inside a unittest?
4. What's the key difference between assert() and enforce() in D?
5. What does 'dub test -b unittest-cov' add over a plain 'dub test'?
Was this page helpful?
You May Also Like
DUB Package Management
DUB is D's official build tool and package manager -- it resolves dependencies, drives builds across configurations, and publishes reusable packages to the DUB registry.
D and C Interop
How D's native extern(C) linkage lets you call existing C libraries directly and expose D functions to C code without writing wrapper glue.
BetterC and Systems Programming
The -betterC compiler switch strips D down to a GC-free, runtime-minimal subset ideal for embedded, kernel, and other systems-programming contexts where a full runtime isn't an option.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics