Overview
Codebelt.Extensions.Xunit provides the foundational Test base class for all xUnit test classes in the Codebelt ecosystem. It replaces ad-hoc disposal patterns with a deterministic lifecycle: override OnDisposeManagedResources to release managed resources, OnDisposeManagedResourcesAsync for async cleanup, or OnDisposeUnmanagedResources for native handles. The base class exposes TestOutput so every test method can write human-readable diagnostics through xUnit's ITestOutputHelper.
Beyond lifecycle management, the package supplies Match for wildcard-based string assertions with configurable symbols, InMemoryTestStore<T> for collecting and querying typed objects during a test run, and WriteLines extension methods that write one line per value to the test output. These APIs together form the base layer that Codebelt.Extensions.Xunit.Hosting and Codebelt.Extensions.Xunit.Hosting.AspNetCore build upon.
Key APIs
Test is the abstract base class every test class should derive from. Its constructor accepts an ITestOutputHelper and stores it in the protected TestOutput property. The HasTestOutput property reports whether output is available, and CallerType returns the concrete test class type. The class implements both IDisposable and IAsyncDisposable with three protected hooks: OnDisposeManagedResources() for synchronous managed cleanup, OnDisposeManagedResourcesAsync() for async managed cleanup, and OnDisposeUnmanagedResources() for releasing native handles. The Disposed property tracks whether disposal has occurred, and InitializeAsync() is called by xUnit before the first test runs.
TestOutput (the protected ITestOutputHelper property inherited from Test) lets test methods write diagnostic output visible in test runners. Use TestOutput.WriteLine for a single line or call the WriteLines extension method to write one line per item in a collection. WriteLines accepts params object[], T[], or IEnumerable<T>.
Match is a static method on Test that compares an expected string against an actual string with optional wildcard support. By default, * matches any group of characters and ? matches a single character. Pass a WildcardOptions delegate to change the symbols or set ThrowOnNoMatch to true so a mismatch throws ArgumentOutOfRangeException with the non-matching lines in ActualValue.
InMemoryTestStore<T> implements ITestStore<T> and provides an in-process collection for test scenarios. Call Add to store items, Query to filter with an optional predicate, or QueryFor<TResult> to retrieve items by their concrete type. The Count property reports the number of stored items.
ITest defines the minimal contract for test classes: CallerType, IDisposable, and IAsyncDisposable.
Basic usage
using System;
using System.Collections.Generic;
using Codebelt.Extensions.Xunit;
using Xunit;
namespace MyProject.Tests;
public class OrderProcessor
{
public record OrderResult(string OrderId, int ItemCount, decimal Total);
public OrderResult Process(string customerName, IEnumerable<(string Name, int Qty, decimal Price)> items)
{
var itemList = new List<(string Name, int Qty, decimal Price)>();
foreach (var item in items) { itemList.Add(item); }
var total = 0m;
foreach (var item in itemList) { total += item.Qty * item.Price; }
return new OrderResult(Guid.NewGuid().ToString("N"), itemList.Count, total);
}
}
public class OrderProcessorTest : Test
{
private readonly OrderProcessor _processor = new();
public OrderProcessorTest(ITestOutputHelper output) : base(output)
{
}
[Fact]
public void Process_MultipleItems_ReturnsCorrectTotalAndCount()
{
var result = _processor.Process("Alice", new[]
{
("Widget", 3, 9.99m),
("Gadget", 1, 24.50m),
("Doohickey", 2, 4.75m)
});
TestOutput.WriteLine($"Query: Alice");
TestOutput.WriteLine($"Count: {result.ItemCount}");
TestOutput.WriteLines($"OrderId: {result.OrderId}", $"Total: {result.Total}");
Assert.Equal(3, result.ItemCount);
Assert.Equal(63.97m, result.Total);
Assert.True(Match("OrderId: *", $"OrderId: {result.OrderId}"));
}
}
Use this pattern when you want deterministic resource cleanup and diagnostic output in every test class without repeating boilerplate disposal code. When your tests do not need structured disposal or custom test output, plain xUnit test classes with no base class are sufficient.
Installation
dotnet add package Codebelt.Extensions.Xunit
Usage guidance
Derive your test classes from Test when you need a consistent disposal lifecycle across your test suite, particularly when tests allocate streams, connections, file handles, or other resources that must be released deterministically. The OnDisposeManagedResources and OnDisposeManagedResourcesAsync hooks replace scattered try/finally blocks, and TestOutput gives every test method a direct path to xUnit's diagnostic output. If your tests only need to assert values and do not allocate disposable resources, inheriting from Test adds overhead that plain xUnit classes avoid. For tests that require dependency injection or ASP.NET Core hosting, use Codebelt.Extensions.Xunit.Hosting or Codebelt.Extensions.Xunit.Hosting.AspNetCore instead, which build on this package's Test base class.