Bunsen Burner

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 xUnit Facts; 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