Codebelt

Codebelt.Extensions.Xunit

Foundational base class, disposable lifecycle, and an in-memory store for writing consistent xUnit v3 tests.

.NET 10.0 / .NET 9.0 / .NET Standard 2.0 MIT v11.1.0 71,731 downloads

Overview

Codebelt.Extensions.Xunit is the foundation of the family. It supplies a common base class for xUnit v3 tests, a deterministic disposable lifecycle, wildcard string matching, and a small in-memory store that other packages build upon. The goal is a uniform shape for test classes so that managed, unmanaged, and asynchronous cleanup follow the same pattern across a codebase.

Every other package in this repository derives its test base classes and logging stores from the types defined here, which makes this package the right starting point when you only need plain unit tests without Microsoft Dependency Injection or ASP.NET Core hosting.

Key APIs

Test is the abstract base class from which test classes derive. Its constructor accepts an ITestOutputHelper output (initialized automatically by xUnit) and an optional caller Type, exposes the output through the protected TestOutput property and the HasTestOutput flag, and tracks teardown through the Disposed property. It implements both IDisposable and IAsyncDisposable together with xUnit's IAsyncLifetime.

Test.OnDisposeManagedResources, OnDisposeManagedResourcesAsync, and OnDisposeUnmanagedResources are the protected virtual hooks you override to release resources. The base class guards against double disposal with a lock and the Disposed flag, so overrides run at most once and can mix synchronous and asynchronous cleanup.

Test.Match is a static method that compares an expected string against an actual string with wildcard support, returning true or false. It accepts an optional Action<WildcardOptions> to tune matching behavior, which is useful for asserting generated text that contains variable fragments such as timestamps or identifiers.

WildcardOptions configures Test.Match through GroupOfCharacters (default \*), SingleCharacter (default \?), and ThrowOnNoMatch. Setting ThrowOnNoMatch to true makes a failed match throw ArgumentOutOfRangeException with the non-matching lines reported in ActualValue.

InMemoryTestStore<T> is the default implementation of ITestStore<T>. It collects items through Add, reports Count, filters with Query(Func<T, bool> predicate = null), and narrows by concrete type with QueryFor<TResult>() where TResult : T. The protected InnerStore property is the extension point for derived stores.

TestOutputHelperExtensions.WriteLines extends ITestOutputHelper to write one line per value, with overloads for params object[], a typed array, and IEnumerable<T>. ITestOutputHelperAccessor and its default TestOutputHelperAccessor provide AsyncLocal-backed access to the current output helper, which the hosting packages use to route logger output to the active test.

Basic usage

using System.Linq;
using Codebelt.Extensions.Xunit;
using Xunit;

namespace AuditTests;

public class AuditTrailTest : Test
{
    public AuditTrailTest(ITestOutputHelper output) : base(output)
    {
    }

    [Fact]
    public void Withdraw_ShouldRecordEachTransaction()
    {
        var audit = new InMemoryTestStore<string>();
        var account = new Account(audit);

        account.Withdraw(100);
        account.Withdraw(50);

        var entries = audit.Query().ToList();
        TestOutput.WriteLines(entries);

        Assert.Equal(2, audit.Count);
        Assert.Contains("Withdrew 100", entries);
    }

    private sealed class Account
    {
        private readonly InMemoryTestStore<string> _audit;

        public Account(InMemoryTestStore<string> audit) => _audit = audit;

        public void Withdraw(int amount) => _audit.Add($"Withdrew {amount}");
    }
}

Use this pattern when a system under test emits events, log lines, or records that you want to capture and assert after the fact rather than mocking a collaborator. It matters because InMemoryTestStore<T> gives the producer a real ITestStore<T> to write to while the test queries the same store through a stable, type-safe API.

Installation

dotnet add package Codebelt.Extensions.Xunit

Usage guidance

Reach for this package when you want a consistent base class and deterministic disposal for plain unit tests, especially in libraries that target .NET Standard 2.0 alongside modern .NET. If your tests need Microsoft Dependency Injection, a running IHost, or an ASP.NET Core request pipeline, prefer Codebelt.Extensions.Xunit.Hosting or Codebelt.Extensions.Xunit.Hosting.AspNetCore, which extend the Test base class defined here.

Family packages