KnockOff 0.49.0
See the version list below for details.
dotnet add package KnockOff --version 0.49.0
NuGet\Install-Package KnockOff -Version 0.49.0
<PackageReference Include="KnockOff" Version="0.49.0" />
<PackageVersion Include="KnockOff" Version="0.49.0" />
<PackageReference Include="KnockOff" />
paket add KnockOff --version 0.49.0
#r "nuget: KnockOff, 0.49.0"
#:package KnockOff@0.49.0
#addin nuget:?package=KnockOff&version=0.49.0
#tool nuget:?package=KnockOff&version=0.49.0
KnockOff
A .NET mocking library that lets you define reusable stub classes — with full mocking capabilities built in.
Define your test double once. Reuse it across your test project. Customize it per-test with Return, Call, Verify, and When chains. No more copying mock setups between tests or maintaining shared factory methods full of Arg.Any<>().
Powered by Roslyn source generation for tighter type safety — more issues surface as compile errors instead of runtime surprises.
Claude Code was used to write this library. Skip to more AI discussion.
KnockOff Stub
There are 9 patterns total, including a standard fluent mocking approach with inline stubs. But reusable stub classes are where KnockOff stands apart:
[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);
}
}
[KnockOff]+partial class— KnockOff generates a base class that implements every member ofIMyRepo. Your stub is a real class — define it once, reuse it across your entire test project. Pass it around, register it in DI, share it between test fixtures.- Constructor parameters —
List<User> Usersis a primary constructor. Test data flows in naturally, just like any other C# class. - Overrides are optional —
GetUser_andUpdate_override the generated defaults. Only override what you need — everything else still works with Return/Call, Return(value), or When chains. - Tighter type safety — Every Return, Call, and When call is complete in a single step — no forgotten
.Returns()that silently breaks at runtime. No manual<T1, T2>type parameters that can drift. Details →
This stub is also a full mock. It has Verify, Strict mode, Async, and Source Delegation — all on the same reusable class.
Why I Wrote KnockOff
I often wanted to reuse my mocks. Especially in my integration test library where I may even register my mocks. I found myself either copying my mock definitions code or creating shared methods like this:
NSubstitute:
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;
}
Here's another example from PowerToys.
But I find that hard to read and unintuitive. Also, my shared methods accumulated extra parameters for variations across different tests.
So I Created KnockOff
You can create a stub to implement interfaces or non-sealed classes with virtual methods. Yet, you can still customize the stub per test. All while having the features you would expect with a full mocking library.
With the stub above, your tests are:
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(Called.Once);
Need different behavior for a specific test? Override with Return/Call:
var user1 = new User { Id = 1 }; // Ignored do to per-test configuration
var myRepoKO = new MyRepoStub([user1]);
var userDomainModel = new UserDomainModel(myRepoKO);
var user2 = new User { Id = 2 };
// When and Return overrides the stub methods
myRepoKO.GetUser.When(2).Return(user2).Verifiable();
myRepoKO.Update.Call(u => Assert.Same(u, user2)).Verifiable();
userDomainModel.Fetch(2);
userDomainModel.Update();
myRepoKO.Verify();
Now I have my stubs and mocks in one!
What Sets KnockOff Apart
- Reusable stub classes — Define once, customize per-test. Your stub is a real class — pass it through constructors, register it in DI.
- Source delegation — Delegate to a real implementation, override only specific methods. No equivalent in Moq or NSubstitute.
- Protected methods — Same
Return/Call/VerifyAPI, fully typed. No string-based names, no manual subclasses. - Ref/out parameters — Natural lambda syntax with
ref/outkeywords. No special matchers or index-based access. - Multiple interfaces — Unified interceptors on one stub. No
.As<T>()references or casting. - Tighter type safety — Each Return/Call/When call is complete in one step — no forgotten
.Returns()that silently breaks at runtime. - Parameter matching —
Return((a, b) => a > 0 ? 100 : 0)— standard C# conditionals instead ofArg.Is<>orIt.Is<>per parameter. - Built-in argument capture —
LastArg,LastArgs,LastSetValue,LastSetEntry— no manualArg.Do<>orCallback<>setup. - Event verification —
VerifyAdd()/VerifyRemove()/HasSubscribers— not available in Moq or NSubstitute. - Explicit Get/Set verification —
VerifyGet(Called)/VerifySet(Called)for properties and indexers. - Stubbing concrete classes — Override virtual methods on non-sealed classes with the same API.
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_WithReturn()
{
var stub = new QuickStartRepoStub();
stub.GetUser.Return((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.Return((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();
}
The Difference
Moq:
mock.Setup(x => x.GetUser(It.Is<int>(id => id > 0)))
.Returns<int>(id => new User { Id = id });
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.Return((id) => id > 0 ? new User { Id = id } : null);
No It.Is<>(). No Arg.Is<>(). No x.Arg<int>(). The parameter is just id.
For side-by-side comparison tables (methods, properties, events, delegates, indexers), see the complete comparison guide.
Argument Matching
Moq:
// Moq - It.Is<T> per parameter
mock.Setup(x => x.Add(It.Is<int>(a => a > 0), It.IsAny<int>())).Returns(100);
NSubstitute:
// NSubstitute - Arg.Is<T> per parameter (permanent matchers)
calc.Add(Arg.Is<int>(a => a > 0), Arg.Any<int>()).Returns(100);
KnockOff:
// KnockOff - Returns with conditional (permanent, matches all calls)
stub.Add.Return((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).Return(100).ThenCall((a, b) => a + b);
Multiple specific values:
Moq:
mock.Setup(x => x.Add(1, 2)).Returns(100);
mock.Setup(x => x.Add(3, 4)).Returns(200);
// Multiple specific values
calc.Add(1, 2).Returns(100);
calc.Add(3, 4).Returns(200);
stub.Add.When(1, 2).Return(100);
stub.Add.When(3, 4).Return(200);
Note: Moq and NSubstitute matchers are permanent -- they match all qualifying calls. KnockOff's When() is sequential -- matchers are consumed in order. Use Return(callback) with conditionals for permanent matching behavior.
Argument Capture
Moq:
// Moq - requires Callback setup
int capturedA = 0, capturedB = 0;
mock.Setup(x => x.Add(It.IsAny<int>(), It.IsAny<int>()))
.Callback<int, int>((a, b) => { capturedA = a; capturedB = b; });
mock.Object.Add(1, 2);
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.Return((a, b) => a + b);
ICalculator calc = stub;
calc.Add(1, 2);
var (a, b) = tracking.LastArgs; // Named tuple: a = 1, b = 2
For full comparisons of properties, events, delegates, and indexers, see the complete comparison guide.
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
Moq:
// It.IsAny<T>() required - compiler needs the types to resolve overload
mock.Setup(x => x.Format(It.IsAny<string>(), It.IsAny<bool>())).Returns("bool overload");
mock.Setup(x => x.Format(It.IsAny<string>(), It.IsAny<int>())).Returns("int overload");
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.Return((string input, bool uppercase) => "bool overload");
stub.Format.Return((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).Return("UPPERCASE");
stub.Format.When("test", 10).Return("truncated");
Argument Access
Moq:
// To use argument values, extract via Returns<T1, T2>:
mock.Setup(x => x.Format(It.IsAny<string>(), It.IsAny<bool>()))
.Returns<string, bool>((input, uppercase) => uppercase ? input.ToUpper() : input);
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.Return((string input, bool uppercase) => uppercase ? input.ToUpper() : input);
The Difference:
- Moq:
It.IsAny<bool>()+.Returns<string, bool>((input, uppercase) => ...)to match any value and access arguments - 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
Three Stub Patterns
KnockOff supports 9 patterns total. Here are the three most common:
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.Return((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.Return((id) => new User { Id = id });
MyService service = stub.Object;
Assert.NotNull(service.GetUser(1));
}
Roslyn Source Generation
KnockOff uses Roslyn source generation, which means:
- 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
Source generation opens doors beyond traditional mocking — I've already added 9 patterns and features like Source Delegation, with more ideas to come.
What other ideas do you have? Open a discussion.
AI
This is an idea I've had for years but never took the time to implement. With my ideas and guidance, Claude Code has written the entirety of this library — the Roslyn source generator, the runtime library, the tests, and the documentation.
Source generation turned out to be a great fit for AI code generation. The work is highly patterned: analyze an interface, generate code for each member, handle edge cases across 9 patterns and 4 member types. That's exactly the kind of systematic, repetitive-but-varied work where AI excels. I designed the API and patterns; Claude Code implemented them across every combination.
Claude Code Skill
KnockOff includes a Claude Code skill that teaches Claude how to use the library. Copy the skills/knockoff/ directory into your project and Claude Code will know how to create stubs, configure behavior, write tests with KnockOff, and migrate from Moq — without you explaining the API.
The skill includes slash commands:
/knockoff:create-stub— Create a new stub class with the pattern of your choice/knockoff:migrate-from-moq— Convert existing Moq tests to KnockOff/knockoff:troubleshoot— Diagnose and fix common KnockOff issues
Documentation
- Getting Started - Installation and first stub
- Stub Patterns - Standalone, inline interface, inline class
- Interceptor API - Complete
Returns,Execute,Get,Setreference - Source Delegation - Delegate to real implementations
- Full Comparison Guide - Properties, events, delegates, indexers vs Moq and NSubstitute
- 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 |
Reduce generated code size by eliminating thin inner subclasses via generic base class interface implementation. Fix: Verifiable(Called times) no longer silently discards the times parameter.