Skip to content

Getting Started

Installation

dotnet add package Burla

Most users should install Burla and go. If you want the runtime library without IDE analyzers and code fixes, install Burla.Core instead. Install Burla.Analyzers directly only if you explicitly want the IDE suggestions/code fixes package by itself.

The BURLA01x compatibility analyzer rules default to info, so supported IDEs surface them as gentle nudges toward Burla-native spellings. If you are in the middle of a broad Moq migration and want less noise, lower them in .editorconfig:

dotnet_diagnostic.BURLA010.severity = silent
dotnet_diagnostic.BURLA011.severity = silent
dotnet_diagnostic.BURLA012.severity = silent

Use none instead if you want them fully off during migration. BURLA020 stays useful even during migration: it fixes accidentally writing Times.Once, Times.Never, or Times.AtLeastOnce without ().

Burla targets .NET 8.0 and works with .NET 8, 9, and 10 projects.

Migrating from another mocking library or working with an LLM?

If you're coming from Moq or NSubstitute, or you want to hand migration work to an LLM, start with these guides:

Your first mock

using Burla;

public interface IGreeter
{
    string Greet(string name);
}

[Fact]
public void Greet_ReturnsConfiguredMessage()
{
    // 1. Create a mock
    var mock = Mock.Of<IGreeter>();

    // 2. Configure behavior
    mock.Setup(x => x.Greet("World")).Returns("Hello, World!");

    // 3. Get the mock object and use it
    var greeter = mock.Instance;
    var result = greeter.Greet("World");

    // 4. Assert the result
    Assert.Equal("Hello, World!", result);
}

Key concepts:

  • Mock.Of<T>() creates a mock wrapping the interface T
  • .Setup(x => ...) configures what a method returns
  • .Instance gives you the actual instance to pass to code under test (.Object is a compatibility alias, and BURLA010 flags it by default if you keep the analyzer enabled)
  • The mock records every call, so you can verify later

Direct instance helpers

If a test only needs the runtime dependency and does not need later access to Verify, CallsTo, or Reset—especially in setup-only or NSubstitute-style tests—use Mock.Create<T>() or Mock.CreateLoose<T>():

var greeter = Mock.Create<IGreeter>(m =>
{
    m.Setup(x => x.Greet("World")).Returns("Hello, World!");
});

Assert.Equal("Hello, World!", greeter.Greet("World"));

var calculator = Mock.CreateLoose<ICalculator>();
Assert.Equal(0, calculator.Add(1, 2));

Keep Mock.Of<T>() when you need the wrapper later, and use mock.Instance as the runtime value.

Strict vs Loose mode

Burla defaults to strict mode. Unconfigured calls throw UnexpectedCallException:

var mock = Mock.Of<ICalculator>(); // strict by default
mock.Setup(x => x.Add(1, 1)).Returns(2);

mock.Instance.Add(1, 1); // ✅ returns 2
mock.Instance.Add(2, 2); // 💥 throws UnexpectedCallException

Switch to loose mode to return defaults for unconfigured calls:

var mock = Mock.Of<ICalculator>(MockBehavior.Loose);

mock.Instance.Add(1, 1); // returns 0 (default for int)

Or use the shorthand:

var mock = Mock.OfLoose<ICalculator>();

Why strict by default?

Strict mode catches bugs where your code calls methods you didn't expect. With loose mode (Moq's default), those calls silently return default — which can hide real issues.

Void methods in strict mode

In strict mode, even void methods need a setup to be allowed:

var mock = Mock.Of<INotificationService>();
mock.Setup(x => x.Send(Arg.Any<string>()));  // allows the call
mock.Instance.Send("hello"); // OK

// Without setup:
// mock.Instance.Send("hello"); // throws UnexpectedCallException!

Callbacks

Use typed callbacks to capture arguments passed to methods:

// Void method: typed callback captures arguments
var mock = Mock.Of<INotificationService>();
string? captured = null;

mock.Setup(x => x.Send(Arg.Any<string>()))
    .Callback<string>(msg => captured = msg);

mock.Instance.Send("hello");
Assert.Equal("hello", captured);
// Return method: callback runs after the value is returned
int capturedA = 0, capturedB = 0;
mock.Setup(x => x.Add(Arg.Any<int>(), Arg.Any<int>()))
    .Returns(0)
    .Callback<int, int>((a, b) => { capturedA = a; capturedB = b; });

Typed callback overloads go up to 4 arguments. Untyped Callback(Action) is also available when you don't need the arguments.

Argument matchers

Match any argument:

mock.Setup(x => x.Add(Arg.Any<int>(), Arg.Any<int>())).Returns(42);

Match with a predicate:

mock.Setup(x => x.Add(Arg.Is<int>(n => n > 0), Arg.Any<int>())).Returns(999);

Match from a set of values:

mock.Setup(x => x.GetStatus(Arg.IsIn("active", "pending"))).Returns("OK");

Match null or non-null:

mock.Setup(x => x.Send(Arg.IsNull<string>())).Throws<ArgumentNullException>();
mock.Setup(x => x.Send(Arg.IsNotNull<string>())).Callback<string>(s => log.Add(s));

Exact values work too — just pass the value directly:

mock.Setup(x => x.Add(2, 3)).Returns(5);

Arg matchers also work from helper methods, not just directly in the lambda:

// Helper that wraps a matcher
private static string IsNonEmpty() => Arg.Is<string>(s => !string.IsNullOrEmpty(s));

// Works in both Setup and CallsTo
mock.Setup(x => x.Send(IsNonEmpty())).Callback(() => { });
var calls = mock.CallsTo(x => x.Send(IsNonEmpty()));

Burla-native docs use Arg for matchers. If you are migrating from Moq, It is available as a compatibility alias, but Arg remains the primary style in new Burla code.

Verifying calls

Burla uses a query-and-assert pattern. Instead of a custom verification API, query the recorded calls and use standard xUnit assertions:

var mock = Mock.Of<INotificationService>();
mock.Setup(x => x.Send(Arg.Any<string>()));

mock.Instance.Send("hello");
mock.Instance.Send("world");

// Query calls matching a pattern
var calls = mock.CallsTo(x => x.Send(Arg.Any<string>()));
Assert.Equal(2, calls.Count);

// Query specific calls
var helloCalls = mock.CallsTo(x => x.Send("hello"));
Assert.Single(helloCalls);

// Verify nothing was called with a specific argument
var errorCalls = mock.CallsTo(x => x.Send("error"));
Assert.Empty(errorCalls);

Burla-native tests usually stop at CallsTo(...) + normal assertions.

If you are migrating from Moq or want to use VerifyNoOtherCalls, Burla also provides Verify(...):

mock.Verify(x => x.Send("hello"), Times.Once());
mock.Verify(x => x.Send("error"), Times.Never());

Use VerifyNoOtherCalls when you want to account for every interaction:

mock.Instance.Send("hello");
mock.Instance.Send("world");

mock.Verify(x => x.Send(Arg.Any<string>()), Times.Exactly(2));
mock.VerifyNoOtherCalls(); // passes — all calls are accounted for

When call order matters, create a sequence and attach setups with InSequence(...):

using var sequence = Mock.Sequence();

var notifications = Mock.Of<INotificationService>();
var audit = Mock.Of<IAuditSink>();

notifications.Setup(x => x.Send("starting")).InSequence(sequence);
audit.Setup(x => x.Write("started")).InSequence(sequence);

notifications.Instance.Send("starting");
audit.Instance.Write("started");

Out-of-order calls throw VerificationException immediately. Disposing the sequence (via using) also verifies that no expected ordered step was skipped.

Use Reset() to clear setups, recorded calls, verification state, and subscribed event handlers:

mock.Reset();
// Existing setups and recorded calls are gone.
// The mock object is the same, and settings like CallBase are unchanged.

Async methods

For methods returning Task<T> or ValueTask<T>, just use Returns with the plain value — Burla wraps it automatically:

var mock = Mock.Of<IAsyncDataService>();
mock.Setup(x => x.GetDataAsync(1)).Returns("data-1"); // auto-wraps in Task.FromResult

var result = await mock.Instance.GetDataAsync(1); // "data-1"

ReturnsAsync is also available if you prefer being explicit:

mock.Setup(x => x.GetDataAsync(1)).ReturnsAsync("data-1");

ValueTask<T> works the same way:

mock.Setup(x => x.GetCountAsync()).Returns(42);

IAsyncEnumerable

This is Burla's killer feature — first-class support with no helper methods:

var mock = Mock.Of<IAsyncDataService>();
mock.Setup(x => x.StreamDataAsync(Arg.Any<CancellationToken>()))
    .ReturnsAsyncEnumerable("item1", "item2", "item3");

var items = new List<string>();
await foreach (var item in mock.Instance.StreamDataAsync())
{
    items.Add(item);
}

Assert.Equal(["item1", "item2", "item3"], items);

Compare this with Moq/NSubstitute, where you'd need to write a separate async iterator helper method.

Throwing exceptions

// Throw a specific exception instance
mock.Setup(x => x.Divide(Arg.Any<int>(), 0))
    .Throws(new DivideByZeroException("Cannot divide by zero"));

// Throw by type
mock.Setup(x => x.Send("error")).Throws<InvalidOperationException>();

// Throw with a factory that receives the method arguments
mock.Setup(x => x.Divide(Arg.Any<int>(), Arg.Any<int>()))
    .Throws<int, int, InvalidOperationException>((a, b) =>
        new InvalidOperationException($"Cannot divide {a} by {b}"));

Properties

Properties are set up just like methods:

var mock = Mock.Of<IConfigService>();
mock.Setup(x => x.BaseUrl).Returns("https://api.example.com");
mock.Setup(x => x.Timeout).Returns(30);

Assert.Equal("https://api.example.com", mock.Instance.BaseUrl);

In strict mode, property setters also need explicit setup:

var mock = Mock.Of<IConfigService>();
mock.Setup(x => x.BaseUrl).Returns("https://api.example.com");
mock.SetupSet<string>(x => x.BaseUrl);

mock.Instance.BaseUrl = "https://staging.example.com"; // allowed

Ref and out parameters

Use Arg.Ref<T>.Any in the setup expression and SetsByRefParameter(index, value) to write back through the parameter slot:

var mock = Mock.Of<IParser>();

mock.Setup(x => x.TryParse("42", out Arg.Ref<int>.Any))
    .Returns(true)
    .SetsByRefParameter(1, 42);

Assert.True(mock.Instance.TryParse("42", out var value));
Assert.Equal(42, value);

The same API works for ref parameters too:

mock.Setup(x => x.Transform(ref Arg.Ref<string>.Any))
    .SetsByRefParameter(0, "TRANSFORMED");

Events

Subscribe the normal way, then emit through mock.Event(name).Emit(...):

var mock = Mock.Of<IEventPublisher>();
string? received = null;

mock.Instance.MessageReceived += (_, message) => received = message;
mock.Event(nameof(IEventPublisher.MessageReceived)).Emit(mock.Instance, "hello");

Assert.Equal("hello", received);

Burla checks that the event name exists and that the supplied arguments match the handler signature.

Sequences

Return different values on consecutive calls:

var mock = Mock.Of<ICalculator>();
mock.Setup(x => x.Add(1, 1)).ReturnsSequence(10, 20, 30);

mock.Instance.Add(1, 1); // 10
mock.Instance.Add(1, 1); // 20
mock.Instance.Add(1, 1); // 30
mock.Instance.Add(1, 1); // 💥 throws SequenceExhaustedException

Control what happens after all values are used:

// Return a specific value after exhaustion
mock.Setup(x => x.Add(1, 1)).ReturnsSequence(10, 20).ThenReturns(0);

// Throw after exhaustion
mock.Setup(x => x.Add(1, 1)).ReturnsSequence(10, 20).ThenThrows<InvalidOperationException>();

Mocking classes

Burla can mock abstract and concrete classes, not just interfaces:

// Abstract class
var mock = Mock.Of<MyAbstractService>();
mock.Setup(x => x.GetData()).Returns("mocked");
var result = mock.Instance.GetData(); // "mocked"

// Concrete class with virtual methods
var mock = Mock.Of<ConcreteService>();
mock.Setup(x => x.VirtualMethod()).Returns(42);

// Or call the base implementation by default
var partial = Mock.Of<ConcreteService>();
partial.CallBase = true;
var status = partial.Instance.GetStatus(); // real virtual implementation runs

// With constructor arguments
var mock = Mock.Of<StorageBase>("connectionString", 30);
mock.Setup(x => x.Connect()).ReturnsAsync(true);

Only virtual methods can be mocked

Non-virtual methods on concrete classes run the original implementation. Sealed classes cannot be mocked.

Next steps