D Language Testing Guide: unittest Blocks, fluent-asserts, and dunit
D has built-in testing via unittest blocks — no framework needed for basic tests. For richer assertions and test organization, fluent-asserts provides a fluent API and dunit adds xUnit-style classes with setup/teardown. This guide covers all three approaches with practical examples.
Key Takeaways
D's unittest blocks are compiled only when run with -unittest. Production builds don't include test code. This is similar to Rust's #[cfg(test)] attribute — test code is never in your release binary.
assert in D is an assertion, not a test framework. assert(x == 5) throws an AssertError with line number information. It's sufficient for simple tests but gives poor failure messages for complex cases.
fluent-asserts gives expression-level failure messages. x.should.equal(5) reports "Expected 5 but got 3". This is the library to reach for when assert messages aren't descriptive enough.
dunit provides @Before, @After, and suite organization. If you're coming from Java/JUnit, dunit feels familiar. Classes annotate test methods with UDAs (User-Defined Attributes).
Run D unit tests with dub test. If using DUB (D's package manager), dub test compiles and runs all unittest blocks in your package.
D's Built-in unittest Blocks
D has test support at the language level. unittest blocks can appear anywhere in a D source file:
// src/math.d
module math;
int add(int a, int b) {
return a + b;
}
int factorial(int n) {
if (n < 0) throw new Exception("factorial requires non-negative input");
if (n <= 1) return 1;
return n * factorial(n - 1);
}
unittest {
// Basic assertions
assert(add(2, 3) == 5);
assert(add(-1, 1) == 0);
assert(add(0, 0) == 0);
}
unittest {
// Factorial tests
assert(factorial(0) == 1);
assert(factorial(1) == 1);
assert(factorial(5) == 120);
}
unittest {
// Exception test
import std.exception : assertThrown;
assertThrown!Exception(factorial(-1));
}Run tests:
# With dmd
dmd -unittest -main src/math.d && ./math
<span class="hljs-comment"># With DUB
dub <span class="hljs-built_in">testBuilding Tests with DUB
// dub.json
{
"name": "myapp",
"targetType": "executable",
"mainSourceFile": "src/app.d",
"dependencies": {},
"configurations": [
{
"name": "unittest",
"targetType": "executable",
"sourceFiles": ["src/math.d", "src/parser.d"]
}
]
}dub test <span class="hljs-comment"># Runs all unittest blocks
dub <span class="hljs-built_in">test --verbose <span class="hljs-comment"># Verbose outputAssertion Functions in std.exception
The standard library provides several assertion helpers:
import std.exception;
unittest {
// assertThrown — verify an exception is raised
assertThrown!Exception(doSomethingBad());
// assertThrown with specific exception type
assertThrown!RangeError(array[-1]);
// assertNotThrown — verify code runs without exceptions
assertNotThrown!Exception(safeOperation());
// enforce — assert with a specific exception
enforce(x > 0, "x must be positive"); // Throws Exception if false
enforce!ValueError(x > 0, "x must be positive"); // Specific type
}fluent-asserts Setup
For richer assertion messages, install fluent-asserts:
// dub.json
{
"dependencies": {
"fluent-asserts": "~>1.0.0"
}
}// tests/test_math.d
import fluent.asserts;
import math;
unittest {
add(2, 3).should.equal(5);
add(-1, 1).should.equal(0);
}
unittest {
// Collection assertions
auto items = [1, 2, 3, 4, 5];
items.should.containOnly([1, 2, 3, 4, 5]);
items.should.contain(3);
items.length.should.equal(5);
}
unittest {
// String assertions
auto name = "Alice";
name.should.equal("Alice");
name.should.startWith("Al");
name.should.endWith("ce");
name.should.contain("lic");
}
unittest {
// Exception assertions
({
factorial(-1);
}).should.throwException!Exception;
({
factorial(-1);
}).should.throwException!Exception.withMessage("factorial requires non-negative input");
}The key advantage: when add(2, 3).should.equal(6) fails, fluent-asserts prints:
Expected 6 but got 5.Not just "AssertError@test_math.d(5)".
dunit — xUnit-style Testing
dunit provides class-based test organization similar to JUnit:
// dub.json
{
"dependencies": {
"dunit": "~>1.0.0"
}
}// tests/test_user.d
import dunit;
import user;
class UserTests {
mixin UnitTest;
User testUser;
@Before
void setup() {
testUser = new User("alice@example.com", "Alice");
}
@After
void teardown() {
testUser = null;
}
@Test
void testUserHasEmail() {
assertEquals("alice@example.com", testUser.email);
}
@Test
void testUserHasName() {
assertEquals("Alice", testUser.name);
}
@Test
void testUserIsActiveByDefault() {
assertTrue(testUser.isActive);
}
@Test
void testUserCanBeDeactivated() {
testUser.deactivate();
assertFalse(testUser.isActive);
}
@Test
@Ignore("not implemented yet")
void testUserPermissions() {
// pending
}
}dunit assertion methods:
assertEquals(expected, actual)
assertNotEquals(expected, actual)
assertTrue(condition)
assertFalse(condition)
assertNull(value)
assertNotNull(value)
assertSame(a, b) // Reference equality
assertThrows!ExType(expr) // Exception assertionOrganizing unittest Blocks
For complex modules, organize unittest blocks by feature:
// src/inventory.d
module inventory;
import std.exception;
struct Inventory {
private int[string] items;
void addItem(string name, int count) {
enforce(count > 0, "Count must be positive");
items[name] = items.get(name, 0) + count;
}
int getCount(string name) {
return items.get(name, 0);
}
void removeItem(string name, int count) {
enforce(name in items, "Item not found: " ~ name);
enforce(items[name] >= count, "Insufficient quantity");
items[name] -= count;
if (items[name] == 0) items.remove(name);
}
}
// Group 1: addItem
unittest {
auto inv = Inventory();
inv.addItem("sword", 1);
assert(inv.getCount("sword") == 1);
inv.addItem("sword", 2);
assert(inv.getCount("sword") == 3, "Should accumulate counts");
}
// Group 2: removeItem
unittest {
import std.exception : assertThrown;
auto inv = Inventory();
inv.addItem("potion", 5);
inv.removeItem("potion", 3);
assert(inv.getCount("potion") == 2);
assertThrown!Exception(inv.removeItem("missing", 1));
assertThrown!Exception(inv.removeItem("potion", 999));
}
// Group 3: edge cases
unittest {
import std.exception : assertThrown;
auto inv = Inventory();
assertThrown!Exception(inv.addItem("item", 0));
assertThrown!Exception(inv.addItem("item", -1));
assert(inv.getCount("nonexistent") == 0, "Missing item should return 0");
}CI Integration
# .github/workflows/test.yml
name: D Language Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install D compiler
uses: dlang-community/setup-dlang@v1
with:
compiler: dmd-latest
- name: Run tests
run: dub test --build=unittestChoosing Between unittest, fluent-asserts, and dunit
| unittest (built-in) | fluent-asserts | dunit | |
|---|---|---|---|
| Installation | None needed | DUB package | DUB package |
| API style | Procedural assert() |
Fluent chain | xUnit class-based |
| Failure messages | File + line number | Expression + diff | Method + diff |
| Setup/teardown | None (use local scope) | None | @Before / @After |
| Best for | Simple logic tests | Library code | Complex class testing |
Recommendation:
- Start with built-in
unittestblocks andstd.exception.assertThrown - Add
fluent-assertswhen assertion messages become too terse - Use
dunitonly if you're building a large test suite and need class-level organization
Summary
D offers three testing levels:
- Built-in
unittestblocks — zero overhead, compiled out of release builds, sufficient for most cases - fluent-asserts — expressive failure messages, chainable assertions
- dunit — xUnit-style organization with
@Before/@Afterlifecycle
Run with dub test in DUB projects. The built-in unittest approach is D's idiomatic choice — use it first, reach for libraries only when needed.