stub0 1.0.23

dotnet add package stub0 --version 1.0.23
                    
NuGet\Install-Package stub0 -Version 1.0.23
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="stub0" Version="1.0.23" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="stub0" Version="1.0.23" />
                    
Directory.Packages.props
<PackageReference Include="stub0" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add stub0 --version 1.0.23
                    
#r "nuget: stub0, 1.0.23"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package stub0@1.0.23
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=stub0&version=1.0.23
                    
Install as a Cake Addin
#tool nuget:?package=stub0&version=1.0.23
                    
Install as a Cake Tool

stub0

stub0 is a deterministic .NET mocking library for unit and integration tests. It starts with ordinary interface mocks and extends to runtime patching when you need to override concrete classes, one real object, static members, or constructors.

The goal is to keep those cases in one small workflow instead of making you switch between a mocking library for the easy path and a separate patching tool for the hard path.

Why stub0

  • Start with simple interface mocks and keep the same API when tests get harder.
  • Patch one existing object or one type inside a using scope with explicit ownership and cleanup.
  • Verify calls, order, and no-other-calls without leaving the same tool.
  • Fail fast with actionable diagnostics when runtime patching is required but the test host is not configured for it.

Pick the lightest API

  • Stub0.CreatePatched<IMyInterface>(): isolated interface mock with no runtime patching.
  • Stub0.CreatePatched<MyClass>(): isolated patched object for a class or abstract class.
  • Stub0.CreatePatchingScope(instance): temporary override for one existing object you already have.
  • Stub0.CreatePatchingScope<MyType>(): temporary override for static members on classes and value types; constructor hooks remain class-only.
  • Stub0.CreatePatchingScope(typeof(MyStaticClass)): temporary override for static members on static classes.
  • Arg, Times, CallContext, and ConstructorContext: argument matching, verification, and advanced behavior.

Install

dotnet add package stub0
<ItemGroup>
  <PackageReference Include="stub0" Version="x.y.z" />
</ItemGroup>

For supported .NET test frameworks (xUnit 2, xUnit 3, NUnit, MSTest, TUnit), the normal path is:

install stub0 -> run tests -> ready to go

stub0 ships build-transitive props, targets, and runsettings. RunSettingsFilePath is applied automatically only when the project is a test project and no custom runsettings path is already set.

Quick tour

The examples below assume using stub0;, a test framework with Assert, and the usual System namespaces.

1. One workflow across interfaces, instances, and statics

stub0 keeps the core shape the same everywhere: pick the target, use On(...) to select a member, Do(...) to provide behavior, and Verify(...) to assert the interaction.

Interface mock
using var clock = Stub0.CreatePatched<IClock>()
    .On(x => x.Hour()).Do(9);

var sut = new GreetingService(clock.Result);

Assert.Equal("Good morning", sut.GetGreeting());
clock.Verify(x => x.Hour(), Times.Once);
Existing object scope
var primary = new LocalClock();
var secondary = new LocalClock();
var frozen = new DateTime(2026, 3, 17, 9, 0, 0, DateTimeKind.Utc);

using var scope = Stub0.CreatePatchingScope(primary)
    .On(x => x.UtcNow()).Do(frozen);

Assert.Equal(frozen, primary.UtcNow());
Assert.NotEqual(frozen, secondary.UtcNow());
scope.Verify(x => x.UtcNow(), Times.Once);
Static scope
var frozen = new DateTime(2030, 1, 1, 0, 0, 0, DateTimeKind.Utc);

using var scope = Stub0.CreatePatchingScope<AppClock>()
    .On(() => AppClock.UtcNow()).Do(frozen);

Assert.Equal(frozen, AppClock.UtcNow());
scope.Verify(() => AppClock.UtcNow(), Times.Once);

CreatePatched<MyClass>() uses the same isolated workflow for concrete or abstract classes. The main difference is that interface-only mocks do not require runtime patching, while concrete classes, existing instances, and type scopes do.

Value types use the same typed static-scope API:

using var scope = Stub0.CreatePatchingScope<DateTime>()
    .On(() => DateTime.Now)
    .Do(new DateTime(2024, 1, 1));

Static classes use the Type overload:

using var scope = Stub0.CreatePatchingScope(typeof(Console))
    .On(() => Console.WriteLine(Arg.Any<string>()))
    .Do((string value) => { });

2. Match arguments and compute results

Use matchers when the exact argument is not known up front:

  • Arg.Any<T>(): any value of that type.
  • Arg.Equal(value): one exact value.
  • Arg.Is<T>(predicate): custom matching logic.
using var discounts = Stub0.CreatePatched<IDiscounts>()
    .On(d => d.Apply(Arg.Is<string>(code => code.StartsWith("VIP")), Arg.Any<decimal>()))
    .Do((string code, decimal subtotal) => subtotal * 0.8m);

Assert.Equal(80m, discounts.Result.Apply("VIP-42", 100m));
discounts.Verify(d => d.Apply(Arg.Is<string>(code => code.StartsWith("VIP")), 100m), Times.Once);

Use Do(value) when the result is fixed. Use Do(...) with a delegate when the behavior needs the input arguments.

3. Throw exceptions or run a sequence

Use Do(() => throw ...) when every matching call should fail. Use DoSequence(...) when the behavior should change over time.

using var api = Stub0.CreatePatched<IRemoteApi>()
    .On(x => x.Load()).DoSequence(seq => seq.Do(() => throw new TimeoutException("Temporary failure")).Do("ok"));

Assert.Throws<TimeoutException>(() => api.Result.Load());
Assert.Equal("ok", api.Result.Load());
Assert.Equal("ok", api.Result.Load());

The last step in a sequence repeats.

4. Tighten verification when the interaction matters

Once the main behavior works, you can make the test stricter with ordering, no-other-calls checks, and direct inspection of the recorded invocation log.

using var workflow = Stub0.CreatePatched<IWorkflow>()
    .On(x => x.Start()).Do(() => { })
    .On(x => x.Finish()).Do(() => { });

var instance = workflow.Result;

instance.Start();
instance.Finish();

workflow.VerifyOrder(x => x.Start(), x => x.Finish());
workflow.VerifyNoOtherCalls(x => x.Start(), x => x.Finish());

var invocations = workflow.Invocations();
Assert.Equal(2, invocations.Length);

workflow.ClearInvocations();
Assert.Empty(workflow.Invocations());

5. Intercept setters and raise events

Setters, indexers, and event accessors use dedicated helpers when you need to capture or verify them.

var captured = 0;

using var options = Stub0.CreatePatched<RetryOptions>()
    .OnSetter(x => x.MaxRetries, () => Arg.Any<int>())
    .Do((int value) => captured = value);

options.Result.MaxRetries = 3;

Assert.Equal(3, captured);
options.VerifySetter(x => x.MaxRetries, () => 3, Times.Once);

You can also raise events from a mock or scope:

using var download = Stub0.CreatePatched<IDownload>();
var instance = download.Result;
var raised = 0;

instance.Completed += (_, _) => raised++;
download.Raise(nameof(IDownload.Completed), instance, EventArgs.Empty);

Assert.Equal(1, raised);

6. Advanced: use CallContext for name-based setup

On(...) is the best default because it targets one exact member shape. Use On(string).Do((CallContext ctx) => ...) when you intentionally want a name-based rule and need access to the runtime arguments.

var client = new ExchangeRateClient();

using var scope = Stub0.CreatePatchingScope(client)
    .On(nameof(ExchangeRateClient.GetRate)).Do((CallContext ctx) =>
    {
        var currency = (string)ctx.Arguments[0]!;
        return currency == "USD" ? 1.25m : 0.9m;
    });

Assert.Equal(1.25m, client.GetRate("USD"));
Assert.Equal(0.9m, client.GetRate("EUR"));

7. Advanced: wrap detour-backed calls around the original implementation

Use Wrap(...) when you want one around-call primitive instead of separate before/after hooks. CallContext.Arguments stays as the inbound snapshot, while WrapContext.Arg(...) reads and writes the live arguments that Proceed() uses.

var worker = new Worker();

using var scope = Stub0.CreatePatchingScope(worker)
    .On(x => x.Compute(Arg.Any<int>()))
    .Wrap(ctx =>
    {
        ctx.Arg(0, ctx.Arg<int>(0) + 10);

        var result = ctx.Proceed<int>();
        return result + 1;
    });

Task-returning members can also use async wrappers:

var client = new ApiClient();

using var scope = Stub0.CreatePatchingScope(client)
    .On(x => x.LoadAsync())
    .Wrap(async ctx =>
    {
        var value = await ctx.ProceedAsync<string>();
        return value.Trim();
    });

Void-returning members can use the synchronous Action<WrapContext> overload:

using var scope = Stub0.CreatePatchingScope(worker)
    .On(w => w.Notify(Arg.Any<string>()))
    .Wrap(ctx =>
    {
        ctx.Proceed();
    });

Wrap(...) is available only on detour-backed setups: concrete patched classes, non-interface existing-instance scopes, static scopes, and CreatePatchingScope(Type). Interface-only and other proxy-only mocks do not support it.

8. Advanced: observe or replace constructors with runtime patching

Class-based type scopes also expose constructor hooks through BeforeNew(...), AfterNew(...), and New(...).

var seenArgument = -1;
var seenVersion = -1;

{
    using var afterNewScope = Stub0.CreatePatchingScope<Report>().AfterNew(ctx =>
    {
        seenArgument = (int)ctx.Arguments[0]!;
        seenVersion = ((Report)ctx.Instance!).Version;
    });

    _ = new ReportFactory().Create(5);
}

Assert.Equal(5, seenArgument);
Assert.Equal(5, seenVersion);

{
    using var newScope = Stub0.CreatePatchingScope<Report>().New(ctx => { });

    var report = new ReportFactory().Create(5);
    Assert.Equal(0, report.Version);
}

More API surface

  • Arg.Any<T>(), Arg.Equal(value), and Arg.Is<T>(predicate) for argument matching.
  • On(...), Do(...), DoSequence(...), Wrap(...), and Preserve(...) for configuring or calling through behavior.
  • OnSetter(...), OnIndexer(...), OnEventAdd(...), and OnEventRemove(...) for member shapes that do not map cleanly to normal expression syntax.
  • Raise(...) for triggering subscribed event handlers from a mock or scope.
  • Verify(...), VerifyOrder(...), and VerifyNoOtherCalls(...) for interaction checks.
  • Invocations(), SetterInvocations(...), EventAddInvocations(...), and ClearInvocations() for log inspection.
  • On(string).Do((CallContext ctx) => ...) when you intentionally want name-based setup.
  • WrapContext for live argument access plus Proceed() / ProceedAsync().
  • BeforeNew(...), AfterNew(...), New(...), and ConstructorContext for constructor interception.

When runtime patching matters

Interface-only mocking via Stub0.CreatePatched<IMyInterface>() does not require runtime patching.

Runtime patching is required for:

  • Stub0.CreatePatched<T>() when T is a class or abstract class
  • Stub0.CreatePatchingScope(instance) for non-interface instances
  • Stub0.CreatePatchingScope<T>() for static members and constructor hooks
  • Stub0.CreatePatchingScope(typeof(MyStaticClass)) for static members on static classes

When runtime patching is needed, the test process must run with:

  • JIT inlining disabled
  • tiered compilation disabled
  • ReadyToRun disabled
  • dynamic code enabled

stub0 validates those prerequisites and throws with detailed guidance when they are not met.

Docs

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.23 116 10/28/2026
1.0.16 223 3/17/2026
1.0.14 134 3/9/2026
1.0.13 123 3/4/2026
1.0.12 127 3/3/2026
1.0.11 137 2/25/2026
1.0.10 141 2/18/2026
1.0.9 146 1/19/2026