KnockOff 0.36.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package KnockOff --version 0.36.0
                    
NuGet\Install-Package KnockOff -Version 0.36.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.36.0">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="KnockOff" Version="0.36.0" />
                    
Directory.Packages.props
<PackageReference Include="KnockOff">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
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.36.0
                    
#r "nuget: KnockOff, 0.36.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.36.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.36.0
                    
Install as a Cake Addin
#tool nuget:?package=KnockOff&version=0.36.0
                    
Install as a Cake Tool

KnockOff

Why I wrote KnockOff

I found many times I wanted to reuse my mocks. Especially in my integration test library where I may even register my mocks. So, I found myself either copying my mock definitions or creating shared methods like this:

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

But I found these methods quite unreadable and inflexible.

What I really wanted a shared stub:

public class MyRepoManualStub(List<User> Users) : IMyRepo
{
    public User? GetUser(int id)
    {
        return Users.Single(u => u.Id == id);
    }

    public void Update(User user)
    {
        Assert.Contains(user, Users);
    }
}

But that meant:

  • Forced to implement all of the methods of the interface
  • I didn't have any nice to haves like .Verify()

So I created KnockOff

For stand alone stubs KnockOff:

  • Automatically implements all of the interface members
  • Provides features like Verification, Returns and When
  • Allows per-test configuration

With KnockOff the stub looks like:

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

And your test looks like

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

And you don't actually loose the ability to configure per-test!

var user = new User { Id = 1 };
var myRepoKO = new MyRepoStub([user]);
var userDomainModel = new UserDomainModel(myRepoKO);

// OnCall overrides the stub methods
myRepoKO.GetUser.OnCall(id => user).Verifiable();
myRepoKO.Update.OnCall(u => Assert.Same(u, user)).Verifiable();

userDomainModel.Fetch(1);
userDomainModel.Update();

myRepoKO.Verify();

It also uses source generation so:

  • 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

Now I have my stubs and mocks in one!

Plus this is just the start. With source generation I think many more ideas are possible. I've added a number of patterns. And new features like Source delegation.

What other ideas do you have?

AI

With my ideas and guidance Claude Code has written the entirety of this library. What started as a curiosity has shown me the value of AI. These are ideas I've had for years. I would not have been able to actually execute without AI. Even more so in about a month!

NuGet Build Status License: MIT


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 CompareUserRepoStub();
stub.GetUser.OnCall((id) => id > 0 ? new User { Id = id } : null);

No Arg.Is<>(). No x.Arg<int>(). The parameter is just id.


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

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.OnCall((string input, bool uppercase) => "bool overload");
stub.Format.OnCall((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).Returns("UPPERCASE");
stub.Format.When("test", 10).Returns("truncated");

Argument Access

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.OnCall((string input, bool uppercase) => uppercase ? input.ToUpper() : input);

The Difference:

  • 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

Unique Feature: Source Delegation

Delegate to a real implementation, override only what you need:

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(new User { Id = 1 });  // Calls real SimpleUserRepo.Save()
var user = 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.Returns(1, 2, 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:

// NSubstitute - Arg.Is<T> per parameter (permanent matchers)
calc.Add(Arg.Is<int>(a => a > 0), Arg.Any<int>()).Returns(100);

KnockOff:

// 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:

// 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:

// 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.OnCall((a, b) => a + b);
ICalculator calc = stub;
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}");
Sequence factory(Arg.Any<int>()).Returns(1, 2, 3); stub.Interceptor.Returns(1, 2, 3);
Async asyncOp(1).Returns(42); stub.Interceptor.Returns(42); (auto-wraps)
Match values (per-parameter Arg.Is) stub.Interceptor.When(42).Returns("found");
Verify factory.Received()(42); stub.Interceptor.Verify();
Verify count factory.Received(3)(Arg.Any<int>()); stub.Interceptor.Verify(Times.Exactly(3));
Capture (manual with Arg.Do) stub.Interceptor.LastArg (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 Returns(v1, v2, v3) .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.Returns / OnCall / When / Verify 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 ReadmeStandaloneStub : IUserRepo { }

Inline Interface - Test-local stubs:

[Fact]
public void InlineInterface_Pattern()
{
    var stub = new Stubs.IUserRepo();
    stub.GetUser.OnCall((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.OnCall((id) => new User { Id = id });

    MyService service = stub.Object;
    Assert.NotNull(service.GetUser(1));
}

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

Delegate stub rewrite using shared interceptor infrastructure, async API consistency across all 9 patterns, 3 bug fixes (When chain verification, user method type detection, VerifyAll TotalCallCount).