Bunsen Burner
![]()
Arrange, act and assert, enforced by the compiler.
A test DSL for C# that turns arrange-act-assert into a composable pipeline, and disposes everything the arrange and act stages create.
dotnet add package BunsenBurner
Why?
Most tests in C# are written in an arrange, act, assert style, like so,
[Fact(DisplayName = "SerializeAsync can work with anonymous objects")]
public async Task ExampleTest()
{
// Arrange
var widget = new { Name = "Widget1", Cost = 12.50 };
var ms = new MemoryStream();
// Act
await JsonSerializer.SerializeAsync(
ms,
widget,
cancellationToken: TestContext.Current.CancellationToken
);
// Assert
Assert.Equal(
expected: "{\"Name\":\"Widget1\",\"Cost\":12.5}",
actual: Encoding.UTF8.GetString(ms.ToArray())
);
}
That order is a convention. Nothing in the test framework stops a test from asserting before it acts, or from leaking the disposables it created.
Bunsen Burner makes the convention part of the type system:
- arranging, acting and asserting in the wrong order does not compile
- each stage is a delegate, so the compiler and the IDE can lift shared code out of the test into reusable helpers
- everything the arrange and act stages create is disposed at the end of the test
[Fact(DisplayName = "SerializeAsync can work with anonymous objects")]
public Task ExampleTestUsingAaaBuilder2() =>
Arrange(() =>
{
var widget = new { Name = "Widget1", Cost = 12.50 };
var ms = new MemoryStream();
return (widget, ms);
})
// pull out the shared test code
.Act(CallSerializeAsync)
.Assert(result =>
Assert.Equal(expected: "{\"Name\":\"Widget1\",\"Cost\":12.5}", actual: result)
);
// this is now a shared stage and can be used in many tests
private static async Task<string> CallSerializeAsync<T>((T, MemoryStream) data)
{
var (widget, ms) = data;
await JsonSerializer.SerializeAsync(ms, widget);
return Encoding.UTF8.GetString(ms.ToArray());
}
What it does not do
- It is not a test runner. Tests are still
xUnitFacts; the DSL only structures them, so the runner, reporting and CI stay the same - It does not execute anything. Each stage is a delegate the test itself calls
- It does not run tests that do not return a
Task; the DSL is async all the way down - It does not keep disposables alive. Anything disposable created in the arrange
or act stage is disposed unless the asserted stage calls
NoDisposal
See also
- Getting Started - convert a test you already have
- Arrange Act Assert - how the stages chain
- Auto Disposal - what happens to disposable values