Skip to content

Advanced Patterns

Once the basics are in place, Burla gives you a few sharp tools for inspecting calls and tightening behavior without switching to a different API style.

Inspecting RecordedCalls

CallsTo(...) is the usual query-and-assert path, but RecordedCalls exposes every invocation when you need more detail.

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

mock.Instance.SendToUser("user1", "Hello");

var call = mock.RecordedCalls.Single();
Assert.Equal("SendToUser", call.Method.Name);
Assert.Equal("user1", call.GetArgument<string>(0));
Assert.Equal("Hello", call.GetArgument<string>(1));

Each CallRecord includes:

  • Method
  • Arguments
  • ReturnValue
  • Timestamp (UTC)

Use RecordedCalls when you need custom filtering, ordering checks, or argument inspection that does not map cleanly to a single CallsTo(...) query.

Ordered verification with Mock.Sequence()

Most verification in Burla is order-agnostic. When order matters, create a sequence and register 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");

Contract:

  • an out-of-order matching call throws VerificationException immediately
  • disposing the sequence verifies that all registered steps were observed
  • one sequence can coordinate calls across multiple mocks
  • repeated identical steps are matched in registration order

This is the right fit when the sequence itself is part of the behavior you care about.

Last-match-wins overrides

Burla resolves matching setups from newest to oldest. That means the last matching setup wins.

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

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

This makes it easy to layer behavior:

  • start with a broad fallback
  • add specific overrides later
  • keep the most specific or most recent rule closest to the test that needs it
Assert.Equal(2, mock.Instance.Add(1, 1));
Assert.Equal(0, mock.Instance.Add(5, 5));
Assert.Equal(999, mock.Instance.Add(200, 1));

Post-return callbacks

For members that return a value, callbacks run after the return value has been chosen.

var mock = Mock.Of<ICalculator>();
int capturedA = 0, capturedB = 0;

mock.Setup(x => x.Add(Arg.Any<int>(), Arg.Any<int>()))
    .Returns<int, int>((a, b) => a * b)
    .Callback<int, int>((a, b) => { capturedA = a; capturedB = b; });

var result = mock.Instance.Add(3, 7);

Assert.Equal(21, result);
Assert.Equal(3, capturedA);
Assert.Equal(7, capturedB);

Use this when you want both:

  • a computed or fixed return value
  • side effects such as logging, capture, or extra assertions

For void members, use Callback(...) directly on the setup with no Returns(...) step.

Factory returns

Factories are evaluated on each matching call, so they are ideal when the return value depends on arguments or call count.

Per-call state

var mock = Mock.Of<ICalculator>();
int counter = 0;

mock.Setup(x => x.GetId()).Returns(() => ++counter);

Argument forwarding

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

mock.Setup(x => x.GetDataAsync(Arg.Any<int>()))
    .Returns<int>(id => Task.FromResult($"data-{id}"));

Typed overloads go up to four arguments, so you can keep the setup close to the method signature instead of introducing separate helper delegates.

Setting out and ref values with SetsByRefParameter(...)

Burla uses Arg.Ref<T>.Any in the setup expression and SetsByRefParameter(...) to update out or ref parameters.

out parameters

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

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

ref parameters

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

A few rules to keep in mind:

  • the index is zero-based and refers to the parameter position
  • combine SetsByRefParameter(...) with Returns(...) when the method also has a return value
  • multiple setups work well for different input/output combinations
mock.Setup(x => x.TryParse("abc", out Arg.Ref<int>.Any))
    .Returns(false)
    .SetsByRefParameter(1, 0);