KnockOff 0.36.0
See the version list below for details.
dotnet add package KnockOff --version 0.36.0
NuGet\Install-Package KnockOff -Version 0.36.0
<PackageReference Include="KnockOff" Version="0.36.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="KnockOff" Version="0.36.0" />
<PackageReference Include="KnockOff"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add KnockOff --version 0.36.0
#r "nuget: KnockOff, 0.36.0"
#:package KnockOff@0.36.0
#addin nuget:?package=KnockOff&version=0.36.0
#tool nuget:?package=KnockOff&version=0.36.0
KnockOff
Why I wrote KnockOff
I found many times I wanted to reuse my mocks. Especially in my integration test library where I may even register my mocks. So, I found myself either copying my mock definitions or creating shared methods like this:
public static IMyRepo NSubstituteMock(List<User> users)
{
var myRepoMock = Substitute.For<IMyRepo>();
// Setup: configure GetUser to look up from the list based on id
myRepoMock.GetUser(Arg.Any<int>())
.Returns(callInfo => users.SingleOrDefault(u => u.Id == callInfo.Arg<int>()));
// Setup: configure Update to assert user exists in list
myRepoMock.When(x => x.Update(Arg.Any<User>()))
.Do(callInfo => Assert.Contains(callInfo.Arg<User>(), users));
return myRepoMock;
}
But I found these methods quite unreadable and inflexible.
What I really wanted a shared stub:
public class MyRepoManualStub(List<User> Users) : IMyRepo
{
public User? GetUser(int id)
{
return Users.Single(u => u.Id == id);
}
public void Update(User user)
{
Assert.Contains(user, Users);
}
}
But that meant:
- Forced to implement all of the methods of the interface
- I didn't have any nice to haves like .Verify()
So I created KnockOff
For stand alone stubs KnockOff:
- Automatically implements all of the interface members
- Provides features like Verification, Returns and When
- Allows per-test configuration
With KnockOff the stub looks like:
[KnockOff]
public partial class MyRepoStub(List<User> Users) : IMyRepo
{
protected override User? GetUser_(int id)
{
return Users.Single(u => u.Id == id);
}
protected override void Update_(User user)
{
Assert.Contains(user, Users);
}
}
And your test looks like
var myRepoKO = new MyRepoStub([new User { Id = 1 }, new User { Id = 2 }]);
var userDomainModel = new UserDomainModel(myRepoKO);
Assert.True(userDomainModel.Fetch(1));
// I have Verify on my Stub!
myRepoKO.GetUser.Verify(Times.Once);
And you don't actually loose the ability to configure per-test!
var user = new User { Id = 1 };
var myRepoKO = new MyRepoStub([user]);
var userDomainModel = new UserDomainModel(myRepoKO);
// OnCall overrides the stub methods
myRepoKO.GetUser.OnCall(id => user).Verifiable();
myRepoKO.Update.OnCall(u => Assert.Same(u, user)).Verifiable();
userDomainModel.Fetch(1);
userDomainModel.Update();
myRepoKO.Verify();
It also uses source generation so:
- No more
Arg.Any<>(). No moreIt.IsAny<>(). Just write C# - If the method signature changes you get a compile error
- There's a small performance gain but honestly it's negligible
Now I have my stubs and mocks in one!
Plus this is just the start. With source generation I think many more ideas are possible. I've added a number of patterns. And new features like Source delegation.
What other ideas do you have?
AI
With my ideas and guidance Claude Code has written the entirety of this library. What started as a curiosity has shown me the value of AI. These are ideas I've had for years. I would not have been able to actually execute without AI. Even more so in about a month!
The Difference
NSubstitute:
var repo = Substitute.For<IUserRepo>();
repo.GetUser(Arg.Is<int>(id => id > 0)).Returns(x => new User { Id = x.Arg<int>() });
KnockOff:
var stub = new CompareUserRepoStub();
stub.GetUser.OnCall((id) => id > 0 ? new User { Id = id } : null);
No Arg.Is<>(). No x.Arg<int>(). The parameter is just id.
Method Overload Resolution
The Problem: When an interface has overloaded methods with the same parameter count but different types:
public interface IFormatter
{
string Format(string input, bool uppercase);
string Format(string input, int maxLength);
}
Any-Value Matching
NSubstitute:
// Arg.Any<T>() required - compiler needs the types to resolve overload
formatter.Format(Arg.Any<string>(), Arg.Any<bool>()).Returns("bool overload");
formatter.Format(Arg.Any<string>(), Arg.Any<int>()).Returns("int overload");
KnockOff:
// Explicit parameter types resolve the overload - standard C# syntax
stub.Format.OnCall((string input, bool uppercase) => "bool overload");
stub.Format.OnCall((string input, int maxLength) => "int overload");
Specific-Value Matching
NSubstitute:
// Specific value matching - literals work when all args are specific
formatter.Format("test", true).Returns("UPPERCASE");
formatter.Format("test", 10).Returns("truncated");
KnockOff:
// Specific value matching - parameter types resolve the overload
stub.Format.When("test", true).Returns("UPPERCASE");
stub.Format.When("test", 10).Returns("truncated");
Argument Access
NSubstitute:
// To use argument values, extract from CallInfo:
formatter.Format(Arg.Any<string>(), Arg.Any<bool>())
.Returns(x => x.ArgAt<bool>(1) ? x.ArgAt<string>(0).ToUpper() : x.ArgAt<string>(0));
KnockOff:
// Arguments are directly available with names and types:
stub.Format.OnCall((string input, bool uppercase) => uppercase ? input.ToUpper() : input);
The Difference:
- NSubstitute:
Arg.Any<bool>()+x.ArgAt<bool>(1)to match any value and access arguments - KnockOff:
(string input, bool uppercase)- standard C# lambda with named, typed parameters
Unique Feature: Source Delegation
Delegate to a real implementation, override only what you need:
stub.Source(realRepo); // ALL methods delegate to real implementation
// Override just the method you're testing
stub.GetUser.OnCall((id) => new User { Id = id, Name = "Test User" });
IUserRepo repo = stub;
repo.Save(new User { Id = 1 }); // Calls real SimpleUserRepo.Save()
var user = repo.GetUser(1); // Returns test data
No other mocking framework has this. Perfect for integration tests, decorator patterns, and partial mocking without complexity.
Side-by-Side Comparisons
Methods
| Task | NSubstitute | KnockOff |
|---|---|---|
| Return value | calc.Add(1, 2).Returns(3); |
stub.Add.Returns(3); |
| Any argument | calc.Add(Arg.Any<int>(), Arg.Any<int>()).Returns(10); |
stub.Add.Returns(10); |
| Match values | calc.Add(1, 2).Returns(100); |
stub.Add.When(1, 2).Returns(100); |
| Conditional | calc.Add(Arg.Any<int>(), Arg.Any<int>()).Returns(x => ...); |
stub.Add.OnCall((a, b) => a > 0 ? a + b : 0); |
| Throw | calc.Add(Arg.Any<int>(), Arg.Any<int>()).Throws<Exception>(); |
stub.Add.OnCall((a, b) => throw new Exception()); |
| Callback | calc.Add(Arg.Any<int>(), Arg.Any<int>()).Returns(3).AndDoes(x => ...); |
stub.Add.OnCall((a, b) => { log.Add(a); return 3; }); |
| Sequence | calc.Add(1, 2).Returns(1, 2, 3); |
stub.Add.Returns(1, 2, 3); |
| Async | repo.GetUserAsync(1).Returns(user); |
stub.GetUserAsync.Returns(user); |
| Verify called | calc.Received().Add(1, 2); |
stub.Add.Verify(); |
| Verify count | calc.Received(3).Add(Arg.Any<int>(), Arg.Any<int>()); |
stub.Add.Verify(Times.Exactly(3)); |
Argument Matching
NSubstitute:
// NSubstitute - Arg.Is<T> per parameter (permanent matchers)
calc.Add(Arg.Is<int>(a => a > 0), Arg.Any<int>()).Returns(100);
KnockOff:
// KnockOff - OnCall with conditional (permanent, matches all calls)
stub.Add.OnCall((a, b) => a > 0 ? 100 : 0);
// KnockOff - When() for sequential matching (first match returns 100, then falls through)
stub.Add.When((a, b) => a > 0).Returns(100).ThenCall((a, b) => a + b);
Multiple specific values:
// Multiple specific values
calc.Add(1, 2).Returns(100);
calc.Add(3, 4).Returns(200);
stub.Add.When(1, 2).Returns(100);
stub.Add.When(3, 4).Returns(200);
Note: NSubstitute's matchers are permanent—they match all qualifying calls. KnockOff's When() is sequential—matchers are consumed in order. Use OnCall() with conditionals for permanent matching behavior.
Argument Capture
NSubstitute:
// NSubstitute - requires Arg.Do in setup
int capturedA = 0, capturedB = 0;
calc.Add(Arg.Do<int>(x => capturedA = x), Arg.Do<int>(x => capturedB = x));
calc.Add(1, 2);
KnockOff:
// KnockOff - built-in, no pre-setup
var tracking = stub.Add.OnCall((a, b) => a + b);
ICalculator calc = stub;
calc.Add(1, 2);
var (a, b) = tracking.LastArgs; // Named tuple: a = 1, b = 2
Properties
| Task | NSubstitute | KnockOff |
|---|---|---|
| Setup getter | calc.Mode.Returns("Scientific"); |
stub.Mode.OnGet("Scientific"); |
| Setup setter | calc.When(x => x.Mode = Arg.Any<string>()).Do(x => ...); |
stub.Mode.OnSet((v) => captured = v); |
| Verify getter | _ = calc.Received().Mode; |
stub.Mode.VerifyGet(); |
| Verify setter | calc.Received().Mode = "Scientific"; |
stub.Mode.VerifySet(); |
| Verify count | _ = calc.Received(3).Mode; |
stub.Mode.VerifyGet(Times.Exactly(3)); |
| Capture value | calc.When(x => x.Mode = Arg.Do<string>(v => ...)).Do(...); |
stub.Mode.LastSetValue (built-in) |
Events
| Task | NSubstitute | KnockOff |
|---|---|---|
| Raise event | calc.PoweringUp += Raise.Event(); |
stub.PoweringUp.Raise(stub, EventArgs.Empty); |
| Raise with args | calc.PoweringUp += Raise.EventWith(sender, args); |
stub.PoweringUp.Raise(sender, args); |
| Verify subscription | (not available) | stub.PoweringUp.VerifyAdd(Times.Once); |
| Verify unsubscription | (not available) | stub.PoweringUp.VerifyRemove(Times.Once); |
| Check subscribers | (not available) | stub.PoweringUp.HasSubscribers |
Delegates
| Task | NSubstitute | KnockOff |
|---|---|---|
| Setup | factory(Arg.Any<int>()).Returns("result"); |
stub.Interceptor.Returns("result"); |
| With logic | factory(Arg.Is<int>(x => x > 0)).Returns(x => $"val: {x.Arg<int>()}"); |
stub.Interceptor.OnCall((x) => $"val: {x}"); |
| Sequence | factory(Arg.Any<int>()).Returns(1, 2, 3); |
stub.Interceptor.Returns(1, 2, 3); |
| Async | asyncOp(1).Returns(42); |
stub.Interceptor.Returns(42); (auto-wraps) |
| Match values | (per-parameter Arg.Is) | stub.Interceptor.When(42).Returns("found"); |
| Verify | factory.Received()(42); |
stub.Interceptor.Verify(); |
| Verify count | factory.Received(3)(Arg.Any<int>()); |
stub.Interceptor.Verify(Times.Exactly(3)); |
| Capture | (manual with Arg.Do) | stub.Interceptor.LastArg (built-in) |
Indexers
| Task | NSubstitute | KnockOff |
|---|---|---|
| Setup getter | dict["key"].Returns(42); |
stub.Indexer.Backing["key"] = 42; |
| Dynamic getter | dict[Arg.Any<string>()].Returns(0); |
stub.Indexer.OnGet((key) => 0); |
| Verify getter | _ = dict.Received()["key"]; |
stub.Indexer.VerifyGet(); |
| Verify setter | dict.Received()["key"] = 42; |
stub.Indexer.VerifySet(); |
| Capture | (manual with When/Do) | stub.Indexer.LastSetEntry |
Feature Parity
KnockOff covers the features NSubstitute users expect:
| Feature | KnockOff | NSubstitute |
|---|---|---|
| Returns | Returns(value) |
.Returns(value) |
| Returns with logic | OnCall((args) => value) |
.Returns(x => value) |
| Argument matching | When(args).Returns(value) |
Arg.Is<T>() per parameter |
| Sequences | Returns(v1, v2, v3) |
.Returns(v1, v2, v3) |
| Callbacks | Built into OnCall |
.AndDoes(callback) |
| Throws | OnCall(() => throw ...) |
.Throws<T>() |
| Async methods | Auto-wrapped | Auto-wrapped |
| Properties | OnGet / OnSet |
.Returns / assignment |
| Indexers | Indexer.OnGet / OnSet / Backing |
Assignment |
| Events | Raise() / VerifyAdd / VerifyRemove |
Raise.Event() |
| Delegates | Interceptor.Returns / OnCall / When / Verify |
Setup on substitute |
| Verification | .Verify(Times) |
.Received(n) |
| Batch verification | .Verifiable() + stub.Verify() |
Individual .Received() calls |
| Strict mode | [KnockOff(Strict=true)] |
Configure substitute |
What KnockOff Does Better
| Feature | Why It's Better |
|---|---|
| Parameter matching | When((a, b) => a > 0) matches all params at once vs Arg.Is<> per param |
| Named tuple capture | var (a, b) = tracking.LastArgs vs manual Arg.Do<> setup |
| Source delegation | Delegate to real implementation, override specific methods |
| Event verification | VerifyAdd() / VerifyRemove() / HasSubscribers |
| Explicit Get/Set verify | VerifyGet(Times) / VerifySet(Times) |
| Built-in capture | LastArg, LastArgs, LastSetValue, LastSetEntry |
| Reusable stub classes | Define once, customize per-test |
Quick Start
Install
dotnet add package KnockOff
Create a Stub
public interface IQuickStartRepo
{
User? GetUser(int id);
}
[KnockOff]
public partial class QuickStartRepoStub : IQuickStartRepo { }
public class QuickStartCreateStubTests
{
[Fact]
public void CreateStub_IsReady()
{
var stub = new QuickStartRepoStub();
IQuickStartRepo repository = stub;
Assert.NotNull(repository);
}
}
Configure and Verify
[Fact]
public void ConfigureStub_WithOnCall()
{
var stub = new QuickStartRepoStub();
stub.GetUser.OnCall((id) => new User { Id = id, Name = "Test User" });
IQuickStartRepo repository = stub;
var user = repository.GetUser(42);
Assert.NotNull(user);
Assert.Equal(42, user.Id);
Assert.Equal("Test User", user.Name);
}
[Fact]
public void VerifyCalls_WithVerifiable()
{
var stub = new QuickStartRepoStub();
stub.GetUser.OnCall((id) => new User { Id = id, Name = "Test" }).Verifiable();
IQuickStartRepo repository = stub;
var user = repository.GetUser(42);
// Verify() checks all members marked with .Verifiable()
stub.Verify();
}
Three Stub Patterns
Standalone - Reusable across your project:
[KnockOff]
public partial class ReadmeStandaloneStub : IUserRepo { }
Inline Interface - Test-local stubs:
[Fact]
public void InlineInterface_Pattern()
{
var stub = new Stubs.IUserRepo();
stub.GetUser.OnCall((id) => new User { Id = id });
IUserRepo repo = stub;
Assert.NotNull(repo.GetUser(1));
}
Inline Class - Stub virtual members:
[Fact]
public void InlineClass_Pattern()
{
var stub = new Stubs.MyService();
stub.GetUser.OnCall((id) => new User { Id = id });
MyService service = stub.Object;
Assert.NotNull(service.GetUser(1));
}
Documentation
- Getting Started - Installation and first stub
- Stub Patterns - Standalone, inline interface, inline class
- Interceptor API - Complete
OnCall,OnGet,OnSetreference - Source Delegation - Delegate to real implementations
- Migration from Moq - Step-by-step migration guide
- Migration from NSubstitute - Comparison and migration guide
License
MIT License. See LICENSE for details.
Contributing
Contributions welcome! See CONTRIBUTING.md for guidelines.
- Issues: GitHub Issues
- Pull Requests: Bug fixes, features, documentation
- Discussions: GitHub Discussions
| 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
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
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 |
|---|---|---|
| 0.54.0 | 638 | 2/20/2026 |
| 0.52.0 | 139 | 2/18/2026 |
| 0.51.2 | 134 | 2/16/2026 |
| 0.51.1 | 193 | 2/16/2026 |
| 0.51.0 | 142 | 2/16/2026 |
| 0.49.0 | 144 | 2/14/2026 |
| 0.48.0 | 145 | 2/10/2026 |
| 0.47.0 | 138 | 2/10/2026 |
| 0.46.0 | 146 | 2/9/2026 |
| 0.45.0 | 148 | 2/9/2026 |
| 0.44.0 | 143 | 2/8/2026 |
| 0.43.0 | 139 | 2/8/2026 |
| 0.42.0 | 148 | 2/8/2026 |
| 0.40.0 | 138 | 2/8/2026 |
| 0.37.0 | 151 | 2/7/2026 |
| 0.36.0 | 131 | 2/6/2026 |
Delegate stub rewrite using shared interceptor infrastructure, async API consistency across all 9 patterns, 3 bug fixes (When chain verification, user method type detection, VerifyAll TotalCallCount).