Skip to content

feat(assertions): add .because() method for custom failure context #443

@juvistr

Description

@juvistr

Summary

Add a .because(reason) method to all expectation types that allows users to provide custom context for assertion failures.

Motivation

Currently, when an assertion fails, the error message shows the expected vs actual values but lacks additional context about why this assertion matters. Users must rely on spec descriptions for context:

it("should have non-empty cart", () => {
    expect(count).toBeGreaterThan(0);  // No inline context
});

With .because(), users can add context directly to assertions:

expect(count).toBeGreaterThan(0).because("cart should not be empty");
// Error: Expected count to be greater than 0, because cart should not be empty, but was 0

Proposed API

// Works with any expectation type
expect(value).toBe(5).because("user preferences require this value");
expect(items).toContain("apple").because("default fruit must be present");
expect(flag).toBeTrue().because("feature flag should be enabled in test env");
expect(() => action()).toThrow<InvalidOperationException>().because("invalid state should throw");

Implementation Notes

Each expectation struct needs:

  1. A _reason field to store the context
  2. A because(string reason) method that returns a new instance with the reason set
  3. Updated error messages to include the reason when present

Example for Expectation<T>:

public readonly struct Expectation<T>
{
    private readonly string? _reason;

    public Expectation<T> because(string reason)
        => new(Actual, Expression, _isNegated, reason);

    public void toBe(T expected)
    {
        // ... existing logic ...
        var reasonClause = _reason != null ? $", because {_reason}" : "";
        throw new AssertionException(
            $"Expected {Expression} to be {expected}{reasonClause}, but was {Actual}");
    }
}

Files to Modify

  • src/DraftSpec/Expectations/Expectation.cs
  • src/DraftSpec/Expectations/BoolExpectation.cs
  • src/DraftSpec/Expectations/StringExpectation.cs
  • src/DraftSpec/Expectations/CollectionExpectation.cs
  • src/DraftSpec/Expectations/ActionExpectation.cs
  • src/DraftSpec/Expectations/AsyncActionExpectation.cs

Background

This feature is inspired by TUnit's .Because() method. See docs/research/tunit-comparison.md for the full analysis.

Acceptance Criteria

  • All expectation types support .because(reason)
  • Error messages include the reason when provided
  • Reason is optional (existing code unchanged)
  • Works with .not negation: expect(x).not.toBe(5).because("reason")
  • Unit tests cover the new functionality
  • Documentation updated

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions