stub0 1.0.23
dotnet add package stub0 --version 1.0.23
NuGet\Install-Package stub0 -Version 1.0.23
<PackageReference Include="stub0" Version="1.0.23" />
<PackageVersion Include="stub0" Version="1.0.23" />
<PackageReference Include="stub0" />
paket add stub0 --version 1.0.23
#r "nuget: stub0, 1.0.23"
#:package stub0@1.0.23
#addin nuget:?package=stub0&version=1.0.23
#tool nuget:?package=stub0&version=1.0.23
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
usingscope 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, andConstructorContext: 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), andArg.Is<T>(predicate)for argument matching.On(...),Do(...),DoSequence(...),Wrap(...), andPreserve(...)for configuring or calling through behavior.OnSetter(...),OnIndexer(...),OnEventAdd(...), andOnEventRemove(...)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(...), andVerifyNoOtherCalls(...)for interaction checks.Invocations(),SetterInvocations(...),EventAddInvocations(...), andClearInvocations()for log inspection.On(string).Do((CallContext ctx) => ...)when you intentionally want name-based setup.WrapContextfor live argument access plusProceed()/ProceedAsync().BeforeNew(...),AfterNew(...),New(...), andConstructorContextfor 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>()whenTis a class or abstract classStub0.CreatePatchingScope(instance)for non-interface instancesStub0.CreatePatchingScope<T>()for static members and constructor hooksStub0.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 | Versions 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. |
-
net10.0
- Iced (>= 1.21.0)
- JetBrains.Annotations (>= 2025.2.4)
-
net8.0
- Iced (>= 1.21.0)
- JetBrains.Annotations (>= 2025.2.4)
-
net9.0
- Iced (>= 1.21.0)
- JetBrains.Annotations (>= 2025.2.4)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.