advanced20 min

Assertion Design and Building a Small Test DSL

Writing custom domain-level assertions that read like the business language of the product, and when a small, purpose-built DSL genuinely earns its complexity in a test framework.

What you'll learn

  • Design a custom, domain-level assertion that communicates intent more clearly than a generic assertion would
  • Explain the failure-message-quality difference between a generic assertion and a well-designed custom one
  • Explain when a small internal DSL (domain-specific language) is genuinely justified in a test framework, and when it's needless complexity

Prerequisites

Explanation

No real assertion runs against a real system in this lesson's exercises -- they model assertion-design decisions as data, using genuine JavaScript/TypeScript execution.

A generic assertion (expect(enrollment.status).toBe("active")) is functionally correct but communicates relatively little when it fails — the failure message reports that one field didn't match one expected value, with no framing of why that field or that value matters in the business domain. A custom, domain-level assertion (expectEnrollmentToBeActive(enrollment)) wraps the same underlying check but gives it a name and a purpose-built failure message that speaks the product's actual language — "expected the enrollment to be active, but it was 'cancelled'" is immediately meaningful to anyone reading a failed test report, including someone unfamiliar with the raw data shape being checked.

This isn't purely cosmetic — a custom assertion is a genuine, reusable unit of domain knowledge: the logic for "what does it actually mean for an enrollment to be considered active" (maybe it requires status === "active" AND a non-expired expiresAt) lives in exactly one place, rather than being reconstructed, and potentially reconstructed slightly differently or incompletely, inside every test that needs to check it. A change to that business rule (a new condition added to what counts as "active") then requires updating one assertion function, not every test that independently checked the same thing.

Building a small internal DSL — a set of purpose-built helper functions or a fluent chain that reads close to natural, domain language (await enrollAndComplete(user, course), or a fluent given(user).enrolledIn(course).shouldSee(...)) is a genuine, further step past individual custom assertions, and it's a real, honest tradeoff, not an automatic win: a well-designed DSL can make tests dramatically more readable and can encode common workflows in one place — but a DSL is also a small language of its own that every contributor has to learn, and an over-engineered or inconsistent one can end up harder to understand than the plain, explicit code it replaced. A DSL earns its complexity specifically when a workflow or a check recurs constantly across the suite and the plain version has become genuinely repetitive and noisy — not merely because a DSL feels more sophisticated.

Example

Modeling a custom assertion's more meaningful failure message and a DSL-worthiness decision, as data.

function genericAssertionMessage(field, actual, expected) {
  return "Expected " + field + " to be " + JSON.stringify(expected) + " but got " + JSON.stringify(actual);
}
function domainAssertionMessage(enrollment) {
  if (enrollment.status !== "active") {
    return "Expected the enrollment to be active, but it was '" + enrollment.status + "'";
  }
  return null; // passes
}
console.log(genericAssertionMessage("status", "cancelled", "active")); // technically correct, but generic
console.log(domainAssertionMessage({ status: "cancelled" }));           // speaks the product's own language directly

function isDslJustified(workflowRepeatsAcrossManyTests, plainVersionIsNoisy) {
  return workflowRepeatsAcrossManyTests && plainVersionIsNoisy;
}
console.log(isDslJustified(true, true));   // true -- a genuinely recurring, noisy workflow
console.log(isDslJustified(false, false)); // false -- a DSL here would just be needless complexity

Try it yourself

Call domainAssertionMessage with a genuinely active enrollment, and confirm it correctly returns null (no failure).

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Guided exercise

Guided exercise

This models a custom domain assertion combining two conditions only -- no real assertion runs. Write expectEnrollmentActive(enrollment, nowTimestamp): return null if status is 'active' AND expiresAt > nowTimestamp. Otherwise, return a specific message naming which condition failed ('status was ...' or 'expired at ...').

Checks: correctly fails a non-active enrollment with a specific message · correctly fails an active-but-expired enrollment · correctly passes a genuinely active, non-expired enrollment

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Independent exercise

Independent exercise

This models the DSL-worthiness decision with a third factor only -- no real DSL is built. Write isDslJustified(workflowRepeatsAcrossManyTests, plainVersionIsNoisy, teamIsSmallAndUnfamiliar): return true only if the workflow repeats AND the plain version is noisy AND NOT teamIsSmallAndUnfamiliar (a DSL has a real learning-curve cost a small, unfamiliar team may not be ready to pay).

Checks: justifies a DSL when all favorable conditions hold · correctly withholds justification when the team factor is unfavorable, even with a recurring workflow · correctly withholds justification for a non-recurring, non-noisy workflow

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Common mistakes

  • Relying only on generic assertions (expect(x).toBe(y)) for a check that has real domain meaning -- a failed generic assertion communicates far less than a purpose-built domain assertion would to whoever reads the failure later.
  • Reconstructing the same business rule (like 'what counts as an active enrollment') slightly differently across multiple tests, instead of encoding it once in a single, reusable domain assertion.
  • Building an elaborate internal DSL before a workflow has actually proven itself to be genuinely recurring and noisy in its plain form -- a DSL built too early adds a real learning-curve cost without yet having earned it.

Knowledge check

Knowledge check

1. Why does a custom, domain-level assertion (like expectEnrollmentToBeActive) communicate more on failure than a generic one (expect(x).toBe(y))?
2. Why is a custom assertion considered a reusable 'unit of domain knowledge'?
3. What's the honest tradeoff in building a small internal test DSL?

Takeaway

Write custom, domain-level assertions for checks with real business meaning -- they communicate far more on failure and centralize business-rule logic in one place. Build a small internal DSL only once a workflow has proven itself genuinely recurring and noisy in its plain form, and only when the team is positioned to absorb its real learning-curve cost.

Summary

A custom, domain-level assertion (like expectEnrollmentToBeActive) produces a far more meaningful failure message than a generic one, and centralizes a business rule's logic in one reusable place instead of scattering slightly-inconsistent reimplementations across tests. A small internal DSL is a genuine, honest tradeoff between real readability gains and a real learning-curve cost -- it's justified specifically by a workflow that's genuinely recurring and noisy in its plain form, not by DSLs simply feeling more sophisticated.

References

Your notes

Notes save automatically.