KnockOff 0.42.0
See the version list below for details.
dotnet add package KnockOff --version 0.42.0
NuGet\Install-Package KnockOff -Version 0.42.0
<PackageReference Include="KnockOff" Version="0.42.0" />
<PackageVersion Include="KnockOff" Version="0.42.0" />
<PackageReference Include="KnockOff" />
paket add KnockOff --version 0.42.0
#r "nuget: KnockOff, 0.42.0"
#:package KnockOff@0.42.0
#addin nuget:?package=KnockOff&version=0.42.0
#tool nuget:?package=KnockOff&version=0.42.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
Here's what a KnockOff stub looks like. There are 9 patterns total, but this is where it starts:
[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;
}
Moq:
public static Mock<IMyRepo> MoqMock(List<User> users)
{
var mock = new Mock<IMyRepo>();
mock.Setup(x => x.GetUser(It.IsAny<int>()))
.Returns<int>(id => users.SingleOrDefault(u => u.Id == id));
mock.Setup(x => x.Update(It.IsAny<User>()))
.Callback<User>(u => Assert.Contains(u, users));
return mock;
}
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 KnockOff Does Better
| Feature | Why It's Better |
|---|---|
| Reusable stub classes | Define once, customize per-test. No more copying mock setups or shared factory methods. |
| Source delegation | Delegate to a real implementation, override only specific methods. No equivalent in Moq or NSubstitute. |
| Tighter type safety | Each Return/Call/When call is complete in one step — no forgotten .Returns() that silently breaks. No manual <T1,T2> type parameters. |
| Parameter matching | Return((a, b) => a > 0 ? 100 : 0) -- standard C# conditionals instead of Arg.Is<> or It.Is<> per parameter. |
| Built-in capture | LastArg, LastArgs, LastSetValue, LastSetEntry — no manual Arg.Do<> or Callback<> setup. |
| Event verification | VerifyAdd() / VerifyRemove() / HasSubscribers — not available in Moq or NSubstitute. |
| Explicit Get/Set verify | VerifyGet(Called) / VerifySet(Called) for properties and indexers. |
When KnockOff Shines
Reusable test doubles — Define a stub class once with constructor parameters for test data. Use it across hundreds of tests without copying setup code.
Integration tests with DI — Your stub is a real class. Register it in a DI container, pass it through constructors, use it anywhere a real implementation would go.
Partial real implementations — Use Source Delegation to delegate to a real implementation and override only the methods you're testing. Perfect for integration tests and decorator patterns.
Large interfaces — Stub only the methods you care about. Everything else auto-implements with defaults or can be configured with a single Returns call.
Stubbing concrete classes — Override virtual methods on non-sealed classes. KnockOff generates the stub with .Object to access the typed instance.
Unique Feature: Source Delegation
stub.Source(realImplementation) delegates unconfigured method calls to a real object. Configured methods (Return, Call, When) still take priority.
The key: you don't need a complete implementation. KnockOff generates a separate Source() overload for each interface in the hierarchy. Pass an object that implements any interface in the hierarchy — only the matching methods get delegated.
For example, stub an interface that extends IList<string>:
public interface IStepList : IList<string>
{
void AddRange(IEnumerable<string> items);
}
Now pass a List<string> — it implements IList<string> but not IStepList:
var realList = new List<string> { "step1", "step2", "step3" };
// List<string> doesn't implement IStepList, but it does implement IList<string>
// KnockOff delegates IList/ICollection/IEnumerable members to the real list
stub.Source(realList);
IStepList list = stub;
// These work — delegated to List<string>
Assert.Equal(3, list.Count); // ICollection<T>.Count
Assert.Equal("step1", list[0]); // IList<T> indexer
var items = new List<string>();
foreach (var item in list) // IEnumerable<T>
{
items.Add(item);
}
Assert.Equal(new[] { "step1", "step2", "step3" }, items);
// AddRange is NOT delegated — it's on IStepList, which List<string> doesn't implement
// Configure it explicitly, or it returns the smart default
stub.AddRange.Call((newItems) =>
{
foreach (var newItem in newItems)
{
list.Add(newItem);
}
});
Not available in Moq or NSubstitute. See the full Source Delegation guide for hierarchy details, priority order, and complete examples.
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 |
Rename Call(Action) to Return(Action) on Task/ValueTask methods for consistent API naming.