Skip to main content

KnockOff by Keith Voels

NuGet / site data​

Nuget GitHub last commit GitHub Repo stars

Details​

Info​

info

Name: KnockOff

A Roslyn Source Generator for creating unit test stubs. Unlike Moq's fluent runtime configuration, KnockOff uses partial classes for compile-time setup—trading flexibility for readability and performance.

Author: Keith Voels

NuGet: https://www.nuget.org/packages/KnockOff/

You can find more details at https://github.com/NeatooDotNet/KnockOff

Source: https://github.com/NeatooDotNet/KnockOff

Author​

note

Keith Voels Alt text

Original Readme​

note

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.

NuGet Build Status License: MIT

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 of IMyRepo. 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> Users is a primary constructor. Test data flows in naturally, just like any other C# class.
  • Overrides are optional — GetUser_ and Update_ 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/Verify API, fully typed. No string-based names, no manual subclasses.
  • Ref/out parameters — Natural lambda syntax with ref/out keywords. 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 of Arg.Is<> or It.Is<> per parameter.
  • Built-in argument 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 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 more It.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​

License​

MIT License. See LICENSE for details.


Contributing​

Contributions welcome! See CONTRIBUTING.md for guidelines.

About​

note

Generating test stubs with mocking for interfaces

How to use​

Example (source csproj, source files)​

This is the CSharp Project that references KnockOff

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="KnockOff" Version="0.49.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.2.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Mock\MockData.csproj" />
</ItemGroup>
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

</Project>

Generated Files​

Those are taken from $(BaseIntermediateOutputPath)\GX

// <auto-generated/>
#nullable enable

namespace TestClock;

public class QuickStartRepoStubBase
{
/// <summary>Override to provide default implementation for global::MockData.IMyClock.GetNow.</summary>
protected virtual global::System.DateTime GetNow_() => default!;

/// <summary>Override to provide default implementation for global::MockData.IMyClock.GetUtcNow.</summary>
protected virtual global::System.DateTime GetUtcNow_() => default!;

}

Useful​

Download Example (.NET C#)​

Share KnockOff​

https://ignatandrei.github.io/RSCG_Examples/v2/docs/KnockOff

Category "Tests" has the following generators:​

1 Forged Nuget GitHub Repo stars 2026-09-12

2 Imposter Nuget GitHub Repo stars 2025-12-13

3 KnockOff Nuget GitHub Repo stars 2026-02-13

4 mocklis Nuget GitHub Repo stars 2024-01-03

5 MockMe Nuget GitHub Repo stars 2025-02-10

6 MSTest Nuget GitHub Repo stars 2024-04-04

7 Ridge Nuget GitHub Repo stars 2023-08-20

8 Rocks Nuget GitHub Repo stars 2023-04-16

9 SatorImaging.TDoubles Nuget GitHub Repo stars 2026-08-27

10 TinyBDD.MSTest Nuget GitHub Repo stars 2026-07-03

11 TUnit Nuget GitHub Repo stars 2025-11-08

See category​

Tests