Extending
fluent-asserts is designed to be extensible. You can use extension libraries for common frameworks, create custom operations for domain-specific assertions, and add custom serializers for your types.
Extension Libraries
Extension libraries provide pre-built assertions for popular frameworks and types. They register their operations automatically when imported.
Using Extension Libraries
To use an extension library like fluent-asserts-vibe:
- Add the dependency to your
dub.json:
{ "configurations": [ { "name": "unittest", "dependencies": { "fluent-asserts": "*", "fluent-asserts-vibe": "*" } } ]}- Import the extension module in your test file or a shared test fixtures module:
import fluentasserts.vibe.json;- Use the enhanced assertions:
import vibe.data.json;import fluentasserts.vibe.json;
unittest { auto json1 = `{"key": "value"}`.parseJsonString; auto json2 = `{ "key" : "value" }`.parseJsonString;
// JSON equality ignores whitespace differences json1.should.equal(json2);}Creating an Extension Library
Extension libraries follow a simple pattern:
module myextension.json;
import fluentasserts.operations.registry : Registry;import fluentasserts.results.serializers.heap_registry : HeapSerializerRegistry;import fluentasserts.core.evaluation.eval : Evaluation;
// Register operations and serializers on module loadstatic this() { // Register custom serializers for better error output HeapSerializerRegistry.instance.register!MyType(&myTypeSerializer);
// Register custom operations for type pairs Registry.instance.register!(MyType, MyType)("equal", &myTypeEqual); Registry.instance.register!(MyType, string)("equal", &myTypeEqual);}
// Custom serializerHeapString myTypeSerializer(MyType value) { // Convert to string representation}
// Custom operationvoid myTypeEqual(ref Evaluation evaluation) @safe nothrow { // Perform comparison logic}Custom Operations
Operations are functions that perform the actual assertion logic. They receive an Evaluation struct and determine success or failure.
Creating a Custom Operation
import fluentasserts.core.evaluation.eval : Evaluation;
/// Asserts that a string is a valid email address.void beValidEmail(ref Evaluation evaluation) @safe nothrow { import std.regex : ctRegex, matchFirst;
// Get the actual value (use [] to access HeapString content) auto emailSlice = evaluation.currentValue.strValue[];
// Remove quotes from string representation string email; if (emailSlice.length >= 2 && emailSlice[0] == '"' && emailSlice[$-1] == '"') { email = emailSlice[1..$-1].idup; } else { email = emailSlice.idup; }
// Check if it matches email pattern auto emailRegex = ctRegex!`^[^@]+@[^@]+\.[^@]+$`; auto isSuccess = !matchFirst(email, emailRegex).empty;
// Handle negation if (evaluation.isNegated) { isSuccess = !isSuccess; }
// Set error message on failure using FixedAppender if (!isSuccess && !evaluation.isNegated) { evaluation.result.expected.put("a valid email address"); evaluation.result.actual.put(evaluation.currentValue.strValue[]); }
if (!isSuccess && evaluation.isNegated) { evaluation.result.expected.put("not a valid email address"); evaluation.result.actual.put(evaluation.currentValue.strValue[]); }}Registering with UFCS
Use UFCS (Uniform Function Call Syntax) to add the operation to Expect:
import fluentasserts.core.expect : Expect;import fluentasserts.core.evaluator : Evaluator;
// Extend Expect with UFCSEvaluator beValidEmail(ref Expect expect) { expect.evaluation.addOperationName("beValidEmail"); expect.evaluation.result.addText(" be a valid email"); return Evaluator(expect.evaluation, &beValidEmailOp);}
// The operation functionvoid beValidEmailOp(ref Evaluation evaluation) @safe nothrow { // ... implementation as above}Using Your Custom Operation
unittest { expect("user@example.com").to.beValidEmail(); expect("invalid-email").to.not.beValidEmail();}Custom Serializers
Serializers convert values to strings for display in error messages. In v2, use HeapSerializerRegistry for all custom serializers. It provides @nogc compatible serialization using HeapString.
Creating a Custom Serializer
import fluentasserts.results.serializers.heap_registry : HeapSerializerRegistry;import fluentasserts.core.memory.heapstring : HeapString, toHeapString;import std.format : format;
struct User { string name; int age;}
// Register a custom serializer during module initializationstatic this() { HeapSerializerRegistry.instance.register!User((user) { return toHeapString(format!"User(%s, age=%d)"(user.name, user.age)); });}Using Custom Serializers
With the serializer registered, error messages show meaningful output:
unittest { auto alice = User("Alice", 30); auto bob = User("Bob", 25);
expect(alice).to.equal(bob); // Output: // ASSERTION FAILED: alice should equal User(Bob, age=25). // ACTUAL: User(Alice, age=30) // EXPECTED: User(Bob, age=25)}Best Practices
Operation Guidelines
- Make operations
@safe nothrow- Required for reliability - Handle negation - Always check
evaluation.isNegated - Use FixedAppender for results - Use
evaluation.result.expected.put()andevaluation.result.actual.put() - Access HeapString with
[]- Values are stored asHeapString, use the slice operator to access content - Be type-safe - Use D’s type system to catch errors at compile time
Serializer Guidelines
- Keep output concise - Long strings are hard to read in error messages
- Include identifying information - Show what makes values different
- Handle null/empty cases - Don’t crash on edge cases
- Return HeapString - Use
toHeapString()for efficient string handling
Real-World Examples
Domain-Specific Assertions
import fluentasserts.core.evaluation.eval : Evaluation;import std.format : format;
/// Assert that a response has a specific HTTP statusvoid haveStatus(int expectedStatus)(ref Evaluation evaluation) @safe nothrow { // Parse actual status from response (simplified) auto statusStr = evaluation.currentValue.strValue[]; int actualStatus = 0; try { actualStatus = statusStr.to!int; } catch (Exception) { // Handle parse error }
auto isSuccess = actualStatus == expectedStatus; if (evaluation.isNegated) isSuccess = !isSuccess;
if (!isSuccess) { try { evaluation.result.expected.put(format!"HTTP %d"(expectedStatus)); evaluation.result.actual.put(format!"HTTP %d"(actualStatus)); } catch (Exception) { // Handle format error } }}
// Usage:expect(response.status).to.haveStatus!200;expect(errorResponse.status).to.haveStatus!404;Testing Exception Messages
unittest { expect({ throw new Exception("User not found"); }).to.throwException!Exception.withMessage.equal("User not found");}Memory Assertion Extensions
unittest { // Test that a function doesn't allocate GC memory expect({ // Your @nogc code here int sum = 0; foreach (i; 0 .. 100) sum += i; return sum; }).to.not.allocateGCMemory();}Migration from v1
If you have custom operations from v1, you’ll need to update them:
- Import path changed:
fluentasserts.core.evaluationtofluentasserts.core.evaluation.eval - String values are HeapString: Use
evaluation.currentValue.strValue[]instead ofevaluation.currentValue.strValue - Result assignment changed: Use
evaluation.result.expected.put()instead of direct assignment - Serializer registry: Use
HeapSerializerRegistryfor custom serializers
See the Upgrading to v2.0.0 guide for more details.
Next Steps
- See the API Reference for built-in operations
- Read Core Concepts to understand the internals
- Learn about Memory Management for
@nogccontexts