Skip to content

Migrating from NSubstitute

Side-by-side comparison of NSubstitute and Burla patterns. Every example shows the NSubstitute code you have today and the Burla equivalent.

Package change

- <PackageReference Include="NSubstitute" Version="..." />
+ <PackageReference Include="Burla" Version="..." />
- using NSubstitute;
- using NSubstitute.ExceptionExtensions;
+ using Burla;

Creating mocks

var calculator = Substitute.For<ICalculator>();
var mock = Mock.Of<ICalculator>();
var calculator = mock.Instance;

Key difference: wrapper object

NSubstitute returns the mock directly. Burla returns a wrapper (IMock<T>) — use .Instance to get the runtime value (.Object remains as a Moq compatibility alias). The wrapper holds setup and call-recording state.

If you only need the runtime dependency and will not verify through the wrapper later, Mock.CreateLoose<T>() is often the closest Burla convenience helper:

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

Strict by default

NSubstitute has no strict mode — all mocks always return defaults for unconfigured calls. Burla defaults to strict (throws on unconfigured calls). Use Mock.Of<T>(MockBehavior.Loose) for NSubstitute-like behavior.

Setup and returns

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

NSubstitute uses the fluent style directly on the substitute. Burla uses .Setup(x => ...) on the mock wrapper.

Argument matchers

NSubstitute Burla
Arg.Any<int>() Arg.Any<int>()
Arg.Is<int>(n => n > 0) Arg.Is<int>(n => n > 0)
Arg.Is("hello") "hello" (just pass the value)
calculator.Add(Arg.Any<int>(), Arg.Any<int>()).Returns(42);
calculator.Add(Arg.Is<int>(n => n > 0), Arg.Any<int>()).Returns(999);
mock.Setup(x => x.Add(Arg.Any<int>(), Arg.Any<int>())).Returns(42);
mock.Setup(x => x.Add(Arg.Is<int>(n => n > 0), Arg.Any<int>())).Returns(999);

The Arg class is very similar! Burla also adds Arg.IsIn(...), Arg.IsNotIn(...), Arg.IsNull<T>(), and Arg.IsNotNull<T>().

Void methods

// Callback
notificationService.When(x => x.Send(Arg.Any<string>()))
    .Do(callInfo => captured = callInfo.Arg<string>());

// Throw
notificationService.When(x => x.Send("error"))
    .Do(_ => throw new InvalidOperationException());
// Typed callback (equivalent of Arg.Do)
mock.Setup(x => x.Send(Arg.Any<string>()))
    .Callback<string>(msg => captured = msg);

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

Burla uses the same .Setup(x => ...) pattern for void methods. No separate .When(...).Do(...) syntax.

Arg.Do<T> equivalent

NSubstitute's Arg.Do<T>(callback) has no direct equivalent. Use typed Callback<T> instead:

// NSubstitute: service.Send(Arg.Do<string>(x => captured = x));
// Burla:
mock.Setup(x => x.Send(Arg.Any<string>()))
    .Callback<string>(x => captured = x);

Exceptions

using NSubstitute.ExceptionExtensions;

calculator.Divide(Arg.Any<int>(), 0)
    .Throws(new DivideByZeroException("oops"));
mock.Setup(x => x.Divide(Arg.Any<int>(), 0))
    .Throws(new DivideByZeroException("oops"));

Properties

configService.BaseUrl.Returns("https://example.com");
configService.Timeout.Returns(30);

// Setters just work
configService.IsEnabled = true;
mock.Setup(x => x.BaseUrl).Returns("https://example.com");
mock.Setup(x => x.Timeout).Returns(30);

Sequences

calculator.Add(1, 1).Returns(10, 20, 30);
// After exhaustion: repeats last value (30)
mock.Setup(x => x.Add(1, 1)).ReturnsSequence(10, 20, 30);
// After exhaustion: throws SequenceExhaustedException by default

NSubstitute repeats the last value by default. Burla throws an exception by default. Use .ThenRepeatsLast() to match NSubstitute's behavior, or customize via .ThenReturns(value).

Verification

notificationService.Received().Send("test");
notificationService.DidNotReceive().Send(Arg.Any<string>());
notificationService.Received(3).Send(Arg.Any<string>());
// Query and assert (recommended)
Assert.Single(mock.CallsTo(x => x.Send("test")));
Assert.Empty(mock.CallsTo(x => x.Send(Arg.Any<string>())));
Assert.Equal(3, mock.CallsTo(x => x.Send(Arg.Any<string>())).Count);

No Received() syntax

NSubstitute's .Received().Method() pattern doesn't exist in Burla. Burla-native docs use CallsTo(...) + standard assertions. Verify(..., Times...) still exists, but it is mainly there for Moq-style migrations and VerifyNoOtherCalls().

Async methods

dataService.GetDataAsync(1).Returns("data");        // auto-wraps in Task
dataService.GetCountAsync().Returns(42);             // auto-wraps in ValueTask
mock.Setup(x => x.GetDataAsync(1)).Returns("data");            // smart auto-wrap!
mock.Setup(x => x.GetCountAsync()).Returns(42);                 // ValueTask too!

Smart async returns

Like NSubstitute, Burla auto-wraps plain values into Task<T> or ValueTask<T>. ReturnsAsync is also available if you prefer being explicit.

IAsyncEnumerable

// Requires a helper method
dataService.StreamDataAsync(Arg.Any<CancellationToken>())
    .Returns(GetTestDataAsync());

static async IAsyncEnumerable<string> GetTestDataAsync()
{
    yield return "item1";
    await Task.Yield();
    yield return "item2";
    yield return "item3";
}
// First-class support — no helper needed!
mock.Setup(x => x.StreamDataAsync(Arg.Any<CancellationToken>()))
    .ReturnsAsyncEnumerable("item1", "item2", "item3");

Ref/Out parameters

// Out: requires a Returns callback
parser.TryParse("42", out Arg.Any<int>())
    .Returns(x =>
    {
        x[1] = 42;
        return true;
    });

// Ref: limited support — When/Do doesn't propagate well
// Out: clean chaining
mock.Setup(x => x.TryParse("42", out Arg.Ref<int>.Any))
    .Returns(true)
    .SetsByRefParameter(1, 42);

// Ref: same syntax, actually works
mock.Setup(x => x.Transform(ref Arg.Ref<string>.Any))
    .SetsByRefParameter(0, "TRANSFORMED");

Call inspection

var calls = notificationService.ReceivedCalls()
    .Where(c => c.GetMethodInfo().Name == "SendToUser")
    .ToList();

var userId = calls[0].GetArguments()[0]?.ToString();
var calls = mock.RecordedCalls
    .Where(c => c.Method.Name == "SendToUser")
    .ToList();

var userId = calls[0].GetArgument<string>(0);

Events

// Subscribe
eventPublisher.MessageReceived += (sender, msg) => { /* handle */ };

// Raise with event args
eventPublisher.Raise(eventPublisher.MessageReceived += null, new CustomEventArgs("hello"));

// Raise simple EventArgs
eventPublisher.Raise(eventPublisher.MessageReceived += null, EventArgs.Empty);
// Subscribe (same pattern)
mock.Instance.MessageReceived += (sender, msg) => { /* handle */ };

// Emit with Burla's native event API
mock.Event(nameof(IEventPublisher.MessageReceived)).Emit(mock.Instance, "hello");

// For EventHandler<TEventArgs>:
mock.Event(nameof(INotifyPropertyChanged.PropertyChanged)).Emit(mock.Instance, new System.ComponentModel.PropertyChangedEventArgs("PropertyName"));

Burla's event API is mock.Event(name).Emit(...). Pass the event name and then the arguments that would be passed to the event handler (typically sender first, then event data).

Event handler signature matching

The arguments passed to Emit must match the event handler signature. For EventHandler<T>, pass (sender, eventArgs). For custom handlers like Action<T>, pass just the data parameter(s).

Clearing calls (ClearReceivedCallsReset)

sub.ClearReceivedCalls();
mock.Reset();

Reset clears everything

NSubstitute's ClearReceivedCalls() only clears recorded calls. Burla's Reset() also clears setups, verification tracking, and event subscriptions. It does not recreate the mock object or change settings like CallBase.

Class mocking

// Abstract class
var service = Substitute.For<MyAbstractService>();
service.GetData().Returns("mocked");

// Concrete class with constructor args
var repo = Substitute.For<ConcreteRepository>("connStr", 30);
repo.VirtualMethod().Returns(99);
// Abstract class
var mock = Mock.Of<MyAbstractService>(MockBehavior.Loose);
mock.Setup(x => x.GetData()).Returns("mocked");
var service = mock.Instance;

// Concrete class with constructor args
var mock = Mock.Of<ConcreteRepository>(MockBehavior.Loose, "connStr", 30);
mock.Setup(x => x.VirtualMethod()).Returns(99);
var repo = mock.Instance;

Strict by default

NSubstitute class substitutes always return defaults. Burla defaults to strict, so add MockBehavior.Loose when migrating.

Quick reference

NSubstitute Burla
Substitute.For<T>() Mock.CreateLoose<T>() (setup-only) or Mock.Of<T>(MockBehavior.Loose) + .Instance
sub.Method().Returns(val) mock.Setup(x => x.Method()).Returns(val)
Arg.Any<T>() Arg.Any<T>()
Arg.Is<T>(pred) Arg.Is<T>(pred)
sub.Received().Method() Assert.Single(mock.CallsTo(x => x.Method()))
sub.DidNotReceive().Method() Assert.Empty(mock.CallsTo(x => x.Method()))
sub.Received(n).Method() Assert.Equal(n, mock.CallsTo(x => x.Method()).Count)
sub.ReceivedCalls() mock.RecordedCalls
.Returns(a, b, c) .ReturnsSequence(a, b, c)
.When(x => ...).Do(...) .Setup(x => ...).Callback(...)
.When(x => ...).Throw(...) .Setup(x => ...).Throws(...)
Arg.Do<T>(callback) .Callback<T>(callback) (on the setup)
sub.ClearReceivedCalls() mock.Reset()
Substitute.For<AbstractClass>() Mock.Of<AbstractClass>(MockBehavior.Loose) + .Instance
Substitute.For<Class>(args) Mock.Of<Class>(MockBehavior.Loose, args) + .Instance or Mock.CreateLoose<Class>(constructorArgs: args)
No strict mode Strict by default, MockBehavior.Loose opt-in