Prove "zero allocation" in a unit test: exact per-call allocation budgets that fail CI when a hot path regresses, plus snapshot testing for source generators, shipped as source with no runtime dependency.
- A claim becomes a test.
AllocationGateruns a call as many times as you ask and fails if it allocates more bytes per call than you allow. A BenchmarkDotNet[MemoryDiagnoser]run shows a number; this gate fails CI when the number changes. - Exact counts. Synchronous gates count with
GC.GetAllocatedBytesForCurrentThreadafter a forced GC and a warmup, and round the average up, so a recurring allocation never reads as 0 B. - Steady state, not first runs. Each gate measures windows of calls until two in a row agree and keeps the least, so a one-off allocation is not counted. A window that is not yet within budget does not end the gate until tiered compilation has settled, so unoptimised tier-0 code, which can box where the optimised code does not, never fails a budget. A passing gate stops after two windows. A measurement that cannot settle within 20 s throws instead of returning a figure that may be tier-0.
- Async calls too. The
Asyncgates await every call and count allocations on every thread, for code that really yields on I/O. - Source-only, nothing to ship. The helpers compile into your test project as
internalclasses: no runtime DLL, nothing on your public API, andAllocationGatecompiles into a NativeAOT app; CI publishes one with it.
dotnet add package ZeroAlloc.TestHelpersThe package is a development dependency, so it stays private to the project that references it.
using Xunit;
using ZeroAlloc.TestHelpers;
public class FormattingTests
{
private readonly char[] _buffer = new char[16];
[Fact]
public void TryFormat_into_a_buffer_allocates_nothing() =>
AllocationGate.AssertBudget(0, 1_000, () => 12_345.TryFormat(_buffer, out _), "TryFormat");
[Fact]
public void ToString_allocates_exactly_one_string() =>
Assert.Equal(32, AllocationGate.MeasureBytesPerCall(1_000, () => 12_345.ToString()));
}Put 12_345.ToString() under the zero budget and the test fails with the exact cost:
AllocationGate: ToString allocated 32000 B total over 1000 iterations (32 B/call avg, the least of 59 windows), budget is 0 B/call (0 B total).
- Budgets and measurements for
ActionandValueTask<T>calls:AssertBudget,MeasureBytesPerCalland theirValueTaskvariants. AllocationGate - Relative gates:
AssertNoMoreThanfails when a candidate path allocates more than a baseline measured in the same run. - Async gates with a documented method and tolerance, for calls that complete on another thread. Calls that complete asynchronously
TextSnapshotfor string snapshots, with no Roslyn reference. Snapshot testingGeneratorSnapshotfor source-generator output, reading and writing the Verify.SourceGenerators file format, so existing*.verified.csfiles keep working. It is compiled only into projects that reference Roslyn. Installation and packaging
- Documentation: docs/
- Related packages: ZeroAlloc.Analyzers finds allocations at compile time; this package proves they are gone. The ZeroAlloc libraries gate their hot paths with it
- Contributing: issues and pull requests are welcome on GitHub
- License: MIT