D Language Testing Guide: unittest Blocks, fluent-asserts, and dunit

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">test

Building 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 output

Assertion 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 assertion

Organizing 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=unittest

Choosing 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 unittest blocks and std.exception.assertThrown
  • Add fluent-asserts when assertion messages become too terse
  • Use dunit only if you're building a large test suite and need class-level organization

Summary

D offers three testing levels:

  1. Built-in unittest blocks — zero overhead, compiled out of release builds, sufficient for most cases
  2. fluent-asserts — expressive failure messages, chainable assertions
  3. dunit — xUnit-style organization with @Before/@After lifecycle

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.

Read more