Skip to content

API Guide

Complete reference for Burla's public API.

Features

  • Thread-safe: Mocks can be safely used across multiple threads without additional synchronization
  • Strict-by-default: Unconfigured calls throw UnexpectedCallException
  • LLM-friendly: Clear, explicit API designed for easy automated migration

Mock creation

Mock.Of<T>()

Creates a strict mock of interface T. Unconfigured calls throw UnexpectedCallException.

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

Mock.Of<T>(MockBehavior)

Creates a mock with specific behavior.

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

Mock.OfLoose<T>()

Creates a loose mock using the shorthand helper:

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

Mock.Of<T>(params object[] constructorArgs)

Creates a mock of a class, passing arguments to its constructor:

var mock = Mock.Of<StorageBase>("connectionString", 30);

If you need to pass a single null constructor argument, use the explicit named-array form:

var mock = Mock.Of<NullableCtorService>(constructorArgs: [null]);

Mock.Of<T>(MockBehavior, params object[] constructorArgs)

Creates a mock of a class with specific behavior and constructor arguments:

var mock = Mock.Of<StorageBase>(MockBehavior.Loose, "connectionString", 30);

Mock.OfLoose<T>(params object[] constructorArgs)

Creates a loose class mock with constructor arguments:

var mock = Mock.OfLoose<StorageBase>("connectionString", 30);

Mock.Create<T>()

Creates a strict mock and returns the runtime instance directly:

var calculator = Mock.Create<ICalculator>();

You can also configure the wrapper inline before the instance is returned:

var calculator = Mock.Create<ICalculator>(mock =>
{
    mock.Setup(x => x.Add(2, 3)).Returns(5);
});

Mock.CreateLoose<T>()

Creates a loose mock and returns the runtime instance directly:

var calculator = Mock.CreateLoose<ICalculator>();

For class mocks, pass constructor arguments through the optional constructorArgs: parameter.

var storage = Mock.Create<StorageBase>(
    constructorArgs: ["connectionString", 30]);

var configuredStorage = Mock.Create<StorageBase>(
    mock =>
    {
        mock.Setup(x => x.GetData("key")).Returns("value");
    },
    constructorArgs: ["connectionString"]);

When to use Create... helpers

Use Mock.Create<T>() / Mock.CreateLoose<T>() when you only need the runtime dependency, especially in setup-only or NSubstitute-style tests. If you need Verify, CallsTo, Reset, RecordedCalls, or CallBase, keep the IMock<T> from Mock.Of<T>() / Mock.OfLoose<T>() and use mock.Instance.

Mocking classes

Burla supports mocking interfaces, abstract classes, and concrete classes with virtual methods:

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

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

// Class with constructor arguments
var mock = Mock.Of<DatabaseRepository>("server=localhost", TimeSpan.FromSeconds(5));
mock.Setup(x => x.QueryAsync(Arg.Any<string>())).ReturnsAsync(new List<Item>());

Limitations:

  • Sealed classes cannot be mocked — throws ArgumentException
  • Non-virtual methods on concrete classes execute the original implementation and cannot be set up
  • Constructor arguments are matched by type to find the best matching constructor
var mock = Mock.Of<ConcreteService>(MockBehavior.Loose);

// Virtual members can be intercepted
mock.Setup(x => x.GetStatus()).Returns("mocked");
Assert.Equal("mocked", mock.Instance.GetStatus());

// Non-virtual members still run the real implementation
// mock.Setup(x => x.NonVirtualMethod()).Returns("override"); // won't be matched
var result = mock.Instance.NonVirtualMethod();

MockBehavior

Value Behavior
Strict Throws UnexpectedCallException for unconfigured calls (default)
Loose Returns default(T) for unconfigured calls

The mock instance

IMock<T>.Instance

Preferred way to get the runtime instance to pass to code under test.

var mock = Mock.Of<ICalculator>();
var calculator = mock.Instance; // ICalculator instance

IMock<T>.Object

Compatibility alias for Moq-style code. Returns the same runtime instance as .Instance.

var calculator = mock.Object; // same instance as mock.Instance

IMock<T>.CallBase

For class mocks, set CallBase = true to fall back to the base implementation when no setup matches a non-abstract virtual member:

var mock = Mock.Of<ConcreteService>();
mock.CallBase = true;

Assert.Equal("OK", mock.Instance.GetStatus());

// Explicit setups still win
mock.Setup(x => x.GetStatus()).Returns("overridden");
Assert.Equal("overridden", mock.Instance.GetStatus());

CallBase has no effect on interface mocks and cannot make abstract or non-virtual members callable.

Setup

Methods returning a value

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

ISetup<T, TResult> provides:

Method Description
.Returns(value) Return a fixed value
.Returns(Func<TResult>) Compute value on each call
.Returns<T1>(Func<T1, TResult>) Compute value using the method argument
.Returns<T1, T2>(Func<T1, T2, TResult>) Compute value using the method arguments (up to 4 args)
.ReturnsSequence(params TResult[]) Return values from a sequence
.Throws(Exception) Throw a specific exception
.Throws<TException>() Throw by type (creates the exception eagerly)
.Throws<T1, TException>(Func<T1, TException>) Throw with a factory that receives the method argument (up to 4 args)

Factory returns

Factories are evaluated on every matching call:

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

Assert.Equal(1, mock.Instance.GetId());
Assert.Equal(2, mock.Instance.GetId());

Void methods

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

ISetup<T> provides:

Method Description
.Callback(Action) Execute an action when called
.Callback<T1>(Action<T1>) Execute a typed callback that receives the method argument
.Callback<T1, T2>(Action<T1, T2>) Typed callback with method arguments (up to 4 args)
.Throws(Exception) Throw a specific exception
.Throws<TException>() Throw by type (creates the exception eagerly)
.Throws<T1, TException>(Func<T1, TException>) Throw with a factory that receives the method argument (up to 4 args)
.SetsByRefParameter<TOut>(int index, TOut value) Set an out/ref parameter value

Properties

Properties use the same Setup method — pass the property getter:

mock.Setup(x => x.BaseUrl).Returns("https://example.com");
mock.Setup(x => x.Timeout).Returns(30);

For low-diff Moq migrations, SetupGet(...) is also available as a compatibility alias for property getters:

mock.SetupGet(x => x.BaseUrl).Returns("https://example.com");

Burla-native docs still prefer Setup(...) for both methods and property getters.

To allow property setters in strict mode, use SetupSet:

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

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

You can also attach a typed callback to capture the assigned value:

string? captured = null;

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

Last-match-wins

When multiple setups match a call, the last matching setup wins. This lets you set up general behavior first and override for specific cases:

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

mock.Instance.Add(1, 1);     // 2   (second setup matches)
mock.Instance.Add(5, 5);     // 0   (first setup matches)
mock.Instance.Add(200, 1);   // 999 (third setup matches)

Typed Returns (argument forwarding)

Pass method arguments to a factory function:

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

// Two arguments
mock.Setup(x => x.Add(Arg.Any<int>(), Arg.Any<int>()))
    .Returns<int, int>((a, b) => a + b);

Typed overloads go up to 4 arguments: Returns<T1>, Returns<T1, T2>, Returns<T1, T2, T3>, Returns<T1, T2, T3, T4>.

Factory-based Throws

Throw exceptions that depend on the arguments passed to the method:

// Exception factory receives 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}"));

// Single argument
mock.Setup(x => x.Send(Arg.Any<string>()))
    .Throws<string, ArgumentException>(msg =>
        new ArgumentException($"Invalid message: {msg}"));

Factory overloads go up to 4 arguments. Available on both ISetup<T, TResult> (return methods) and ISetup<T> (void methods).

Post-return chaining

After .Returns(...), you can chain:

.Callback(Action) / .Callback<T1, ...>(Action<T1, ...>)

Execute code after returning. Typed overloads receive the method arguments:

int callCount = 0;
mock.Setup(x => x.Add(1, 1)).Returns(2).Callback(() => callCount++);

// Typed: captures the actual arguments
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; });

.SetsByRefParameter<TOut>(int parameterIndex, TOut value)

Set an out/ref parameter on a method that also returns a value:

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

Sequences

Return different values on consecutive calls:

mock.Setup(x => x.Add(1, 1)).ReturnsSequence(10, 20, 30);

ISequenceResult<T, TResult> controls what happens after exhaustion:

Method Behavior after values are used
.ThenRepeatsLast() Repeat the last value (opt-in)
.ThenReturns(value) Return a specific value
.ThenThrows(Exception) Throw a specific exception
.ThenThrows<TException>() Throw by type
(default) Throw SequenceExhaustedException
// After 10 and 20, return 0 forever
mock.Setup(x => x.Add(1, 1)).ReturnsSequence(10, 20).ThenReturns(0);

// After 10 and 20, throw
mock.Setup(x => x.Add(1, 1)).ReturnsSequence(10, 20).ThenThrows<InvalidOperationException>();

Argument matchers

Used in Setup and CallsTo/Verify expressions. Burla-native docs use Arg; It exposes the same matcher surface as low-cost Moq compatibility sugar.

Arg.Any<T>()

Matches any value of type T:

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

Arg.Is<T>(Func<T, bool>)

Matches values satisfying a predicate:

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

Arg.IsIn<T>(params T[])

Matches values in the specified set:

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

Arg.IsNotIn<T>(params T[])

Matches values not in the specified set:

mock.Setup(x => x.GetStatus(Arg.IsNotIn("deleted", "banned"))).Returns("OK");

Arg.IsNull<T>()

Matches a null reference:

mock.Setup(x => x.Send(Arg.IsNull<string>())).Throws<ArgumentNullException>();

Arg.IsNotNull<T>()

Matches any non-null reference:

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

Arg.Ref<T>.Any

Matches any ref or out parameter. Use with ref or out keyword in the expression:

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

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

It

Moq-compatible alias for Arg. Useful when you want migration diffs to stay small:

mock.Setup(x => x.Add(It.IsAny<int>(), It.IsAny<int>())).Returns(42);

Exact values

Pass values directly — they're matched with object.Equals:

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

Ref/Out parameters

Out parameters

Use out Arg.Ref<T>.Any in the setup expression and .SetsByRefParameter(index, value) to configure the output:

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

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

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

mock.Instance.TryParse("42", out var v1);  // true, v1 = 42
mock.Instance.TryParse("abc", out var v2); // false, v2 = 0

Ref parameters

Use ref Arg.Ref<T>.Any to match and .SetsByRefParameter(index, value) to set the new value:

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

var text = "hello";
mock.Instance.Transform(ref text); // text is now "TRANSFORMED"

Async methods

ReturnsAsync

Shorthand for Returns(Task.FromResult(value)) — but you can also just use Returns directly:

// Smart inference — auto-wraps in Task.FromResult:
mock.Setup(x => x.GetDataAsync(1)).Returns("data-1");

// Explicit ReturnsAsync (same result):
mock.Setup(x => x.GetDataAsync(1)).ReturnsAsync("data-1");

ValueTask

Same smart inference works for ValueTask<T>:

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

// Or explicitly:
mock.Setup(x => x.GetCountAsync()).ReturnsAsync(42);
mock.Setup(x => x.GetCountAsync()).Returns(new ValueTask<int>(42));

IAsyncEnumerable

Simple items

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

Custom yield logic

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}");
        }
    });

Delay between items

Useful for testing timeout and cancellation scenarios:

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

Verification

Returns recorded calls matching an expression. Use with standard assertions:

var calls = mock.CallsTo(x => x.Add(Arg.Any<int>(), Arg.Any<int>()));
Assert.Equal(3, calls.Count);

var specificCalls = mock.CallsTo(x => x.Send("hello"));
Assert.Single(specificCalls);

Works with void methods too:

var calls = mock.CallsTo(x => x.Send(Arg.Any<string>()));
Assert.Empty(calls); // never called

For everyday Burla tests, prefer CallsTo(...) + normal test assertions. Verify(...) and Times remain available as optional helpers, mainly for low-diff Moq migrations or for VerifyNoOtherCalls().

Verify (optional helper)

If you want a helper that throws on mismatch, use Verify(...) with a Times constraint:

mock.Verify(x => x.Send("hello"), Times.Once());
mock.Verify(x => x.Send("error"), Times.Never());
mock.Verify(x => x.GetDataAsync(Arg.Any<int>()), Times.AtLeast(2));

Most Burla-native tests can instead assert on CallsTo(...).Count or use helpers such as Assert.Single(...) and Assert.Empty(...).

VerifyNoOtherCalls()

Verifies that no calls were made beyond those already verified via Verify. Throws VerificationException if unverified calls exist:

mock.Instance.Send("hello");
mock.Instance.SendToUser("user1", "world");

mock.Verify(x => x.Send("hello"), Times.Once());
mock.VerifyNoOtherCalls(); // 💥 throws — SendToUser was not verified

Note

CallsTo(...) does not mark calls as verified. Only Verify(...) does. Use Verify before VerifyNoOtherCalls to mark expected calls.

Mock.Sequence() / .InSequence(sequence)

Use ordered setups when call order matters across one or more mocks.

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:

  • InSequence(sequence) registers the setup as the next expected ordered step.
  • An out-of-order matching call throws VerificationException immediately.
  • Disposing the sequence verifies that every registered step was observed.
  • A single sequence can coordinate setups across multiple mocks.

Reset()

Clears mutable mock state: setups, recorded calls, verification tracking, and event subscriptions captured by the mock. Reset() does not recreate the mock object or change other configuration such as CallBase.

var partial = Mock.Of<ConcreteService>();
partial.CallBase = true;

partial.Setup(x => x.VirtualMethod()).Returns(42);
Assert.Equal(42, partial.Instance.VirtualMethod());

partial.Reset();

// All state is gone:
Assert.Empty(partial.RecordedCalls);

// Other configuration is preserved
Assert.True(partial.CallBase);

// With no setup left, class mocks still follow their remaining configuration.
// Here, CallBase stays enabled for later calls to non-abstract virtual members.

// New setups work normally
partial.Setup(x => x.VirtualMethod()).Returns(99);
Assert.Equal(99, partial.Instance.VirtualMethod());

RecordedCalls

Access all recorded calls for advanced inspection:

var allCalls = mock.RecordedCalls;

var sendCalls = allCalls
    .Where(c => c.Method.Name == "SendToUser")
    .ToList();

Assert.Equal("user1", sendCalls[0].GetArgument<string>(0));
Assert.Equal("Hello", sendCalls[0].GetArgument<string>(1));

Times

Used with Verify when you want Moq-style count constraints. Most Burla-native tests can just query calls and use standard assertions.

Factory method Meaning
Times.Never() Must not be called
Times.Once() Exactly 1 call
Times.AtLeastOnce() 1 or more calls
Times.AtLeast(n) At least n calls
Times.AtMost(n) At most n calls
Times.Exactly(n) Exactly n calls
Times.Between(from, to) Between from and to calls (inclusive)

CallRecord

Each recorded invocation is a CallRecord with:

Property Type Description
Method MethodInfo The method that was called
Arguments object?[] The arguments that were passed
ReturnValue object? The value that was returned
Timestamp DateTime When the call was recorded (UTC)

GetArgument<T>(int index)

Typed access to a specific argument:

var call = mock.RecordedCalls.Single();
var name = call.GetArgument<string>(0);
var count = call.GetArgument<int>(1);

Exceptions

UnexpectedCallException

Thrown in strict mode when a call has no matching setup.

Properties:

  • Method — the MethodInfo that was called
  • Arguments — the arguments passed
  • ClosestMatches — read-only list of SetupInfo entries describing the closest matching setups (ranked by relevance). Empty when Burla has no useful suggestions.

Message format (with closest matches):

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.

Message format (no matching setups):

Unexpected call to ICalculator.Add(2, 2). Configure a matching setup or use MockBehavior.Loose.

Closest matches are ranked by relevance: 1. Same method with argument mismatches (highest priority) 2. Different methods with same return type 3. Completely different methods (lowest priority)

VerificationException

Thrown by Verify when the call count doesn't match.

Message format:

Verification failed for ICalculator.Add: expected exactly 1 time(s), but was called 3 time(s).

SequenceExhaustedException

Thrown when a return sequence is exhausted and no exhaustion behavior has been configured.

Properties: - None (standard exception with message only)

Message format:

Return sequence exhausted. The sequence has no more values to return. Configure exhaustion behavior using ThenRepeatsLast(), ThenReturns(value), or ThenThrows(exception).

Constructors: - SequenceExhaustedException() — default message - SequenceExhaustedException(string message) — custom message

Event Emission

Event(string eventName).Emit(params object[] args)

Burla's native event API selects an event by name and then emits it to the current subscribers. The first argument is the event sender (typically mock.Instance), followed by the event arguments.

Contract:

  • Unknown event names throw ArgumentException.
  • Known events with no subscribers are a no-op.
  • The argument count must exactly match the event handler delegate signature.
  • Delegate shapes are not restricted to a special small set; any event delegate arity is supported.
var mock = Mock.Of<IEventPublisher>(MockBehavior.Loose);
string? receivedMessage = null;

mock.Instance.MessageReceived += (sender, msg) => receivedMessage = msg;

mock.Event(nameof(IEventPublisher.MessageReceived)).Emit(mock.Instance, "Hello!");

// receivedMessage is now "Hello!"

For events that use EventArgs, pass an instance of the event args:

var mock = Mock.Of<IButton>(MockBehavior.Loose);
bool clicked = false;

mock.Instance.Clicked += (sender, e) => clicked = true;

mock.Event(nameof(IButton.Clicked)).Emit(mock.Instance, EventArgs.Empty);

// clicked is now true