Skip to content

Troubleshooting

Common Burla failures are usually telling you something useful: a call was stricter than expected, a sequence ran out, or a class member could not be intercepted. This page focuses on the current 1.0.0 behavior.

UnexpectedCallException in strict mode

Burla is strict by default. If a call has no matching setup, Burla throws UnexpectedCallException:

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

mock.Instance.Add(2, 2); // throws UnexpectedCallException

Typical causes:

  • the arguments do not match the setup you wrote
  • a void method was never set up
  • a property setter was used without SetupSet(...)
  • the code under test made one more call than you expected

A few quick fixes:

// Match the exact call
mock.Setup(x => x.Add(2, 2)).Returns(4);

// Or widen the setup with matchers
mock.Setup(x => x.Add(Arg.Any<int>(), Arg.Any<int>())).Returns(0);

// Void methods still need a setup in strict mode
mock.Setup(x => x.Send(Arg.Any<string>()));

If the behavior really should be permissive, switch that mock to loose mode:

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

Understanding the error message

Burla's UnexpectedCallException shows closest matching setups to help you quickly identify what went wrong:

Unexpected call to Calculator.Add(5, 10).

No matching setup found. Closest matches:
  - Calculator.Add(1, 2) → Returns 3 (arg[0] mismatch (expected 1, got 5))
  - Calculator.Add(10, 20) → Returns 30 (arg[0] mismatch (expected 10, got 5))
  - Calculator.Multiply(2, 3) → Returns 6 (method signature mismatch)

Configure a matching setup or use MockBehavior.Loose.

The error message ranks setups by relevance: 1. Same method, wrong arguments - Most likely what you intended 2. Different method, same return type - Possibly a mistake 3. Completely different method - Least likely match

Use this information to: - Check if you made a typo in the method name - Verify your argument matchers are correct - Add the missing setup for the actual call

Return sequence exhausted

ReturnsSequence(...) throws SequenceExhaustedException by default after the last configured value is used:

var mock = Mock.Of<ICalculator>();
mock.Setup(x => x.Add(1, 2)).ReturnsSequence(3);

Assert.Equal(3, mock.Instance.Add(1, 2));
Assert.Throws<SequenceExhaustedException>(() => mock.Instance.Add(1, 2));

This is intentional. Burla does not silently return default or repeat the last value unless you ask it to.

Choose the exhaustion behavior you want up front:

mock.Setup(x => x.Add(1, 2)).ReturnsSequence(3, 5).ThenRepeatsLast();
mock.Setup(x => x.Add(1, 2)).ReturnsSequence(3).ThenReturns(9);
mock.Setup(x => x.Add(1, 2)).ReturnsSequence(3).ThenThrows<InvalidOperationException>();

Use this when the code under test may legitimately call the member more times than your "happy path" setup covered.

Sealed classes and non-virtual members

Burla can mock interfaces, abstract classes, and concrete classes with virtual members. Two common limitations are worth calling out explicitly.

Sealed classes cannot be mocked

var ex = Assert.Throws<ArgumentException>(() => Mock.Of<SealedService>());
Assert.Contains("sealed", ex.Message, StringComparison.OrdinalIgnoreCase);

There is no Burla-specific workaround for this in 1.0.0. Extract an interface, depend on an abstract base class, or wrap the sealed type behind something you can mock.

Non-virtual members are not intercepted

On concrete class mocks, non-virtual members run the real implementation and cannot be overridden with Setup(...).

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

mock.Setup(x => x.GetStatus()).Returns("mocked");   // virtual member
Assert.Equal("mocked", mock.Instance.GetStatus());

var result = mock.Instance.NonVirtualMethod();        // real implementation

CallBase does not change this limitation. It only affects non-abstract virtual members when no setup matches.

Property setters in strict mode

Property getters and setters are configured separately.

A getter setup does not automatically allow assignments:

var mock = Mock.Of<IConfigService>();
mock.Setup(x => x.BaseUrl).Returns("http://test");

Assert.Throws<UnexpectedCallException>(() => mock.Instance.BaseUrl = "http://new-value");

To allow a setter in strict mode, add SetupSet(...):

var mock = Mock.Of<IConfigService>();
mock.Setup(x => x.BaseUrl).Returns("http://test");
mock.SetupSet<string>(x => x.BaseUrl);

mock.Instance.BaseUrl = "http://new-value"; // allowed

If you need to inspect what was assigned, attach a callback:

string? captured = null;

mock.SetupSet<string>(x => x.BaseUrl)
    .Callback<string>(value => captured = value);

Note

SetupSet(...) allows the setter call, but it does not create an automatic backing store. If you want the getter to reflect assigned values, capture them yourself and return them from the getter setup.

If the property does not have a public setter, SetupSet(...) throws ArgumentException.

Async stream timing and cancellation pitfalls

Burla's ReturnsAsyncEnumerable(...) support is first-class, but a few details matter when tests start failing on timing or cancellation.

WithDelayBetweenItems(...) delays only after an item is yielded

The first item is available immediately. The configured delay is applied between items:

mock.Setup(x => x.StreamDataAsync(Arg.Any<CancellationToken>()))
    .ReturnsAsyncEnumerable("a", "b", "c")
    .WithDelayBetweenItems(TimeSpan.FromMilliseconds(50));

If your test cancels before the second item, seeing the first item still arrive is expected.

Pass the cancellation token through the whole enumeration

When testing cancellation, pass the token to the mocked member and to the enumeration:

using var cts = new CancellationTokenSource();

await foreach (var item in mock.Instance.StreamDataAsync(cts.Token)
    .WithCancellation(cts.Token))
{
    // ...
}

This mirrors the existing samples and makes cancellation intent obvious.

Producer overloads must honor the token

If you use the producer overload, the delegate receives a CancellationToken. Use it in your own awaits and checks:

mock.Setup(x => x.StreamDataAsync(Arg.Any<CancellationToken>()))
    .ReturnsAsyncEnumerable(async (yield, ct) =>
    {
        for (int i = 0; i < 5; i++)
        {
            ct.ThrowIfCancellationRequested();
            await yield($"item-{i}");
            await Task.Delay(50, ct);
        }
    });

If you ignore ct, your producer can keep running longer than the test expects.

Disposing the enumerator cancels the producer promptly

Burla links disposal to cancellation for producer-based async streams. If the consumer stops early, dispose the enumerator or exit the await foreach cleanly so background work can stop.