KnockOff 10.33.0

dotnet add package KnockOff --version 10.33.0
                    
NuGet\Install-Package KnockOff -Version 10.33.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="10.33.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="10.33.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 10.33.0
                    
#r "nuget: KnockOff, 10.33.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@10.33.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=10.33.0
                    
Install as a Cake Addin
#tool nuget:?package=KnockOff&version=10.33.0
                    
Install as a Cake Tool

KnockOff

No more Arg.Any<>(). No more It.IsAny<>(). Just write C#.

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

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


Unique Feature: Source Delegation

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

var realRepo = new SqlUserRepository(connectionString);
var stub = new UserRepoStub();

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(user);     // Calls real SqlUserRepository.Save()
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.OnCall((a, b) => 1).ThenCall((a, b) => 2).ThenCall((a, b) => 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 - Arg.Is<T> per parameter (permanent matchers)
calc.Add(Arg.Is<int>(a => a > 0), Arg.Any<int>()).Returns(100);

// 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
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 - 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 - built-in, no pre-setup
var tracking = stub.Add.OnCall((a, b) => a + b);
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}");
Verify factory.Received()(42); stub.Interceptor.Verify();
Capture (manual with Arg.Do) stub.Interceptor.LastCallArg (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 OnCall().ThenCall() .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.OnCall 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 UserRepoStub : IUserRepo { }

Inline Interface - Test-local stubs:

[KnockOff<IUserRepo>]
public partial class MyTests
{
    [Fact]
    public void Test()
    {
        var stub = new Stubs.IUserRepo();
    }
}

Inline Class - Stub virtual members:

[KnockOff<MyService>]
public partial class MyTests
{
    [Fact]
    public void Test()
    {
        var stub = new Stubs.MyService();
        IMyService service = stub.Object;
    }
}

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
10.33.0 82 2/1/2026
10.32.0 79 1/31/2026
10.31.0 84 1/28/2026
10.30.0 83 1/27/2026
10.29.0 84 1/27/2026
10.28.0 88 1/27/2026
10.27.0 87 1/26/2026
10.26.0 95 1/23/2026
10.25.0 92 1/22/2026
10.20.0 92 1/16/2026
10.19.1 84 1/16/2026
10.19.0 92 1/15/2026
10.18.0 86 1/15/2026
10.17.0 91 1/13/2026
10.15.0 97 1/13/2026
10.14.0 84 1/11/2026
10.13.1 94 1/11/2026
10.13.0 85 1/10/2026
10.12.0 102 1/10/2026
10.11.1 90 1/9/2026
10.11.0 85 1/9/2026
10.10.1 88 1/9/2026
10.10.0 90 1/9/2026
10.9.0 92 1/8/2026
10.8.0 89 1/8/2026
10.6.0 87 1/5/2026
10.5.2 88 1/4/2026
10.5.1 91 1/4/2026
10.5.0 88 1/4/2026
10.4.1 87 1/3/2026
10.4.0 90 1/2/2026
10.3.0 91 1/2/2026
10.2.0 94 1/2/2026
10.1.0 95 1/2/2026
10.0.0 96 1/2/2026

User-defined methods now support Verifiable() and participate in Stub.Verify() aggregation.