100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Testing D Code

D has unit testing built directly into the language with unittest blocks, plus contract programming (in/out/invariant) and assert for runtime correctness checks.

Practical DIntermediate8 min readJul 10, 2026
Analogies

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.

d
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.d

Contract 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.

d
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

Was this page helpful?

Topics covered

#Programming#DProgrammingStudyNotes#TestingDCode#Testing#Code#Built#Unittest#StudyNotes#SkillVeris#ExamPrep