KnockOff 10.33.0
dotnet add package KnockOff --version 10.33.0
NuGet\Install-Package KnockOff -Version 10.33.0
<PackageReference Include="KnockOff" Version="10.33.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="KnockOff" Version="10.33.0" />
<PackageReference Include="KnockOff"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add KnockOff --version 10.33.0
#r "nuget: KnockOff, 10.33.0"
#:package KnockOff@10.33.0
#addin nuget:?package=KnockOff&version=10.33.0
#tool nuget:?package=KnockOff&version=10.33.0
KnockOff
No more Arg.Any<>(). No more It.IsAny<>(). Just write C#.
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 UserRepoStub();
stub.GetUser.OnCall((id) => id > 0 ? new User { Id = id } : null);
No Arg.Is<>(). No x.Arg<int>(). The parameter is just id.
Unique Feature: Source Delegation
Delegate to a real implementation, override only what you need:
var realRepo = new SqlUserRepository(connectionString);
var stub = new UserRepoStub();
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(user); // Calls real SqlUserRepository.Save()
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.OnCall((a, b) => 1).ThenCall((a, b) => 2).ThenCall((a, b) => 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 - Arg.Is<T> per parameter (permanent matchers)
calc.Add(Arg.Is<int>(a => a > 0), Arg.Any<int>()).Returns(100);
// 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
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 - 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 - built-in, no pre-setup
var tracking = stub.Add.OnCall((a, b) => a + b);
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}"); |
| Verify | factory.Received()(42); |
stub.Interceptor.Verify(); |
| Capture | (manual with Arg.Do) | stub.Interceptor.LastCallArg (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 | OnCall().ThenCall() |
.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.OnCall |
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 UserRepoStub : IUserRepo { }
Inline Interface - Test-local stubs:
[KnockOff<IUserRepo>]
public partial class MyTests
{
[Fact]
public void Test()
{
var stub = new Stubs.IUserRepo();
}
}
Inline Class - Stub virtual members:
[KnockOff<MyService>]
public partial class MyTests
{
[Fact]
public void Test()
{
var stub = new Stubs.MyService();
IMyService service = stub.Object;
}
}
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 |
|---|---|---|
| 10.33.0 | 82 | 2/1/2026 |
| 10.32.0 | 79 | 1/31/2026 |
| 10.31.0 | 84 | 1/28/2026 |
| 10.30.0 | 83 | 1/27/2026 |
| 10.29.0 | 84 | 1/27/2026 |
| 10.28.0 | 88 | 1/27/2026 |
| 10.27.0 | 87 | 1/26/2026 |
| 10.26.0 | 95 | 1/23/2026 |
| 10.25.0 | 92 | 1/22/2026 |
| 10.20.0 | 92 | 1/16/2026 |
| 10.19.1 | 84 | 1/16/2026 |
| 10.19.0 | 92 | 1/15/2026 |
| 10.18.0 | 86 | 1/15/2026 |
| 10.17.0 | 91 | 1/13/2026 |
| 10.15.0 | 97 | 1/13/2026 |
| 10.14.0 | 84 | 1/11/2026 |
| 10.13.1 | 94 | 1/11/2026 |
| 10.13.0 | 85 | 1/10/2026 |
| 10.12.0 | 102 | 1/10/2026 |
| 10.11.1 | 90 | 1/9/2026 |
| 10.11.0 | 85 | 1/9/2026 |
| 10.10.1 | 88 | 1/9/2026 |
| 10.10.0 | 90 | 1/9/2026 |
| 10.9.0 | 92 | 1/8/2026 |
| 10.8.0 | 89 | 1/8/2026 |
| 10.6.0 | 87 | 1/5/2026 |
| 10.5.2 | 88 | 1/4/2026 |
| 10.5.1 | 91 | 1/4/2026 |
| 10.5.0 | 88 | 1/4/2026 |
| 10.4.1 | 87 | 1/3/2026 |
| 10.4.0 | 90 | 1/2/2026 |
| 10.3.0 | 91 | 1/2/2026 |
| 10.2.0 | 94 | 1/2/2026 |
| 10.1.0 | 95 | 1/2/2026 |
| 10.0.0 | 96 | 1/2/2026 |
User-defined methods now support Verifiable() and participate in Stub.Verify() aggregation.