KnockOff 0.40.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package KnockOff --version 0.40.0
                    
NuGet\Install-Package KnockOff -Version 0.40.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="KnockOff" Version="0.40.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="KnockOff" Version="0.40.0" />
                    
Directory.Packages.props
<PackageReference Include="KnockOff" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add KnockOff --version 0.40.0
                    
#r "nuget: KnockOff, 0.40.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package KnockOff@0.40.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=KnockOff&version=0.40.0
                    
Install as a Cake Addin
#tool nuget:?package=KnockOff&version=0.40.0
                    
Install as a Cake Tool

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

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 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;
}

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(Times.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(Times) / VerifySet(Times) 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 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.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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
Loading failed

Fix CS0108 (missing 'new' keyword on class stub object-override interceptors) and CS1061 (missing 'using System.Linq;' in generated code for overloaded methods).