knockoff

star 2

This skill should be used when the user asks about KnockOff stubs, creating stubs, mocking with KnockOff, KnockOff attributes, Return, Call, Get, Set, setup stub behavior, Verify calls, Verifiable, VerifyAll, track method calls, stub patterns, Stand-Alone pattern, Inline Interface, Inline Class, Inline Delegate, stub a delegate, migrate from Moq, KnockOff async, interceptor API, Strict mode, ThenReturn, ThenCall, ThenGet, ThenSet, generic method interceptor, Source() delegation, When(), argument matching, or needs guidance on creating, configuring, or verifying KnockOff test stubs. When writing tests that need stubs, this skill MUST be consulted to check for existing stubs before creating new inline stubs.

NeatooDotNet By NeatooDotNet schedule Updated 3/8/2026

name: knockoff description: >- This skill should be used when the user asks about KnockOff stubs, creating stubs, mocking with KnockOff, KnockOff attributes, Return, Call, Get, Set, setup stub behavior, Verify calls, Verifiable, VerifyAll, track method calls, stub patterns, Stand-Alone pattern, Inline Interface, Inline Class, Inline Delegate, stub a delegate, migrate from Moq, KnockOff async, interceptor API, Strict mode, ThenReturn, ThenCall, ThenGet, ThenSet, generic method interceptor, Source() delegation, When(), argument matching, or needs guidance on creating, configuring, or verifying KnockOff test stubs. When writing tests that need stubs, this skill MUST be consulted to check for existing stubs before creating new inline stubs.

KnockOff Usage Guide

KnockOff is a Roslyn Source Generator that creates test stubs at compile time. Stubs are reusable, have zero reflection overhead, and provide compile-time safety.

Architecture

Each method on a stubbed interface/class gets a fully generated interceptor class (e.g., AddInterceptor) that inherits from MethodInterceptorRuntime. These interceptors provide:

  • Clean IntelliSense with XML documentation showing original method signatures and parameter names
  • Typed Call/Return/When methods with custom named delegates for parameter IntelliSense
  • Verify(), LastArg/LastArgs, Reset(), Verifiable() directly on the interceptor

Callback parameter conventions:

  • 0 params: () => ...
  • 1 param: Raw type, you name the lambda parameter: (int id) => ...
  • 2+ params: Custom named delegate with typed params: (int a, int b) => a + b
  • ref/out: Custom delegate with ref/out modifiers: (ref int a) => { a = a + 1; }

Overloaded methods use a single interceptor property with overloaded Call/When methods. The lambda signature disambiguates which overload is configured. Call() returns a tracking handle for per-overload Verify()/LastArg/LastArgs.

CRITICAL GOTCHAS

1. Sequences REPEAT Last Value After Exhaustion

stub.Add.Return(1, 999);
calc.Add(0, 0); // Returns 1
calc.Add(0, 0); // Returns 999
calc.Add(0, 0); // Returns 999 (repeats last value!)

// Use ThenDefault() to return default(T) instead of repeating
stub.Add.Return(1, 999).ThenDefault();
calc.Add(0, 0); // Returns 1
calc.Add(0, 0); // Returns 999
calc.Add(0, 0); // Returns 0 (default - ThenDefault() terminates with default)

2. Events Use Raise() and Bare Names

// Events use .Raise() method:
stub.Started.Raise(stub, EventArgs.Empty);
// Event interceptors use the event name directly:
stub.Started.VerifyAdd(Called.Never);
stub.DataReceived.VerifyAdd(Called.Never);

3. Class Stubs Call Base by Default + Use .Object

Class stubs (Patterns 3, 4, 6, 9) call base for unconfigured virtual methods (like Moq's CallBase = true, but default). Abstract methods return default(T).

// WRONG: ServiceBase service = stub;
// RIGHT:
var stub = new Stubs.ServiceBase();
ServiceBase service = stub.Object;
service.Initialize();

4. Closed Generic Stubs Use Simple Names

// For [KnockOff<IRepository<User>>]:
var stub = new Stubs.IRepository();  // NOT Stubs.IRepository<User>

5. Called.Between() Does NOT Exist

// WRONG: Called.Between(1, 5)
// RIGHT: Use separate constraints
stub.Save.Verify(Called.AtLeast(1));
stub.Save.Verify(Called.AtMost(5));

6. Configuration — Last One Wins

All configuration methods use direct replacement. Calling any config method replaces the previous one of the same kind. Known bug: .When() currently accumulates like .ThenWhen() instead of replacing.

7. Set Does NOT Auto-Update Getter

stub.Name.Set((v) => { /* tracks value */ });
service.Name = "test";
// Getter still returns default! Set doesn't update Get
// To link them: stub.Name.Set((v) => stub.Name.Get(v));

8. Reset() Clears Tracking BUT Preserves Config

Reset clears counts, captured args, sequence position, source. Reset preserves Return/Call/Get/Set callbacks, sequence structure, Verifiable marking.


PROACTIVE: Detect Duplicate Inline Stubs

Before creating an inline stub with [KnockOff<T>], always search for existing stubs of that type. If the same type is already stubbed inline elsewhere, recommend creating a standalone stub.


Pattern Selection

Need Pattern Instantiation
Reusable stub across files Standalone new MyStub()
Custom methods on stub Standalone new MyStub()
Generic stub with type params Generic Standalone new MyStub<T>()
Quick test-local stub Inline Interface new Stubs.IService()
Stub a class (virtual/abstract) Inline Class new Stubs.MyClass() then .Object
Stub a delegate Inline Delegate new Stubs.MyDelegate()
Test-local generic interface Open Generic new Stubs.IFoo<T>()

Standalone Pattern

One class declaration per stub. Put the attribute, interface, and any overrides together in a single class declaration.

[KnockOff]
public partial class SkillUserRepoStub : ISkillUserRepo { }
[Fact]
public void StandaloneStub_ConfigureAndVerify()
{
    var stub = new SkillUserRepoStub();
    stub.GetById.Call((id) => new User { Id = id }).Verifiable();
    stub.Save.Call((user) => { }).Verifiable();
    ISkillUserRepo repo = stub;

    var user = repo.GetById(42);
    repo.Save(user!);

    stub.Verify();
}

Inline Interface / Class / Delegate

[KnockOff<ISkillEmailService>]
public partial class SkillEmailTests
{
    [Fact]
    public void Test()
    {
        var stub = new Stubs.ISkillEmailService();
        stub.Send.Call((string to, string subject) => true).Verifiable();
        ISkillEmailService email = stub;
    }
}

Class stubs use .Object: ServiceBase service = stub.Object;

Delegate stubs use stub.Interceptor for config: stub.Interceptor.Return(42);


Method Configuration

stub.GetUser.Return(new User { Id = 1, Name = "Alice" });
// With arguments
stub.GetUser.Call((id) => new User { Id = id, Name = $"User{id}" });

// Void methods
stub.Save.Call((user) => { /* side effects */ });

// Async methods - auto-wrapped, no Task.FromResult needed
stub.GetUserAsync.Call((id) => new User { Id = id });  // Returns Task<User>
stub.SaveAsync.Call((user) => { });  // Returns Task.CompletedTask
// Concise value sequences (preferred)
stub.GetNext.Return(1, 2, 3);
// After third call, repeats 3 (NSubstitute-like behavior)

// Mix callbacks with value sequences
stub.Add.Call((int a, int b) => a + b).ThenReturn(100, 200);
// First: computed, then 100, 200, 200...

// Use ThenDefault() to return default(T) instead of repeating:
stub.GetNext.Return(1, 2).ThenDefault();
// Value matching
stub.GetUser.When(42).Return(adminUser);
stub.GetUser.When(1).Return(regularUser);

// Predicate matching
stub.GetUser.When(id => id < 0).Return(null);

// Chaining
stub.GetUser
    .When(42).Return(adminUser)
    .ThenWhen(id => id > 100).Return(premiumUser)
    .ThenWhen(id => id > 0).Return(regularUser);

// Void methods use Call instead of Return
stub.Log.When("error").Call((msg) => { /* handle */ });

Property Configuration

// Static value
stub.Name.Get("TestName");

// Dynamic callback
stub.Timestamp.Get(() => DateTime.UtcNow);

// Setter interception
stub.Name.Set((value) => capturedValues.Add(value));

// Sequences
stub.Counter.Get(() => 1).ThenGet(() => 2).ThenGet(() => 3);

Indexer Configuration

// Use per-key Returns for specific keys
stub.Indexer["key1"].Returns("value1");
stub.Indexer["key2"].Returns("value2");

// Or use callbacks as fallback for unconfigured keys
stub.Indexer.Get((key) => $"computed-{key}");
stub.Indexer.Set((key, value) => { /* handle */ });

// When(predicate) matches keys by condition
stub.Indexer.When(key => key.StartsWith("prefix_", StringComparison.Ordinal)).Returns("matched");

// Per-key > When > Get callback (priority order)

Event Configuration

// Events use Raise() method
stub.DataReceived.Raise(stub, new DataEventArgs("test-data"));

// Verify subscriptions
stub.DataReceived.VerifyAdd(Called.Once);
stub.DataReceived.VerifyRemove(Called.Never);

Generic Methods

// Use .Of<T>() for type-specific configuration
stub.GetById.Of<User>().Call((id) => new User { Id = id });
stub.GetById.Of<Product>().Call((id) => new Product { Id = id });

// Verify by type
stub.GetById.Of<User>().Verify(Called.Never);
stub.GetById.Of<Product>().Verify(Called.Never);

Delegate Configuration

Delegates use stub.Interceptor. Named delegates only (no Func<>/Action<>). See delegates.md for full reference.

var stub = new Stubs.SkillArithmeticOp();

// Returns (value or callback)
stub.Interceptor.Return(42);
stub.Interceptor.Call((a, b) => a + b);

// Sequences
stub.Interceptor.Return(10, 20, 30);

// When chains
stub.Interceptor.When(1, 2).Return(100)
    .ThenWhen(3, 4).Return(200);

// Async auto-wrapping (for delegates returning Task<T>)
// stub.Interceptor.Return(42);              // auto-wraps in Task.FromResult
// stub.Interceptor.Call((int x) => x * 2); // simplified, auto-wrapped

// Verification (fresh stub for clean tracking)
var verifyStub = new Stubs.SkillArithmeticOp();
verifyStub.Interceptor.Call((a, b) => a + b);
SkillArithmeticOp op = verifyStub;
op(1, 2);
verifyStub.Interceptor.Verify(Called.Once);
Assert.Equal((1, 2), verifyStub.Interceptor.LastArgs);

// Strict mode
stub.Strict = true;

// Implicit conversion to delegate type
SkillArithmeticOp opRef = stub;

Verification

stub.Verify() checks .Verifiable() members. stub.VerifyAll() checks ALL configured members. See verification.md for full reference.

stub.GetUser.Call((id) => new User { Id = id }).Verifiable();
stub.Save.Call((u) => { }).Verifiable(Called.Once);
// ... exercise stub ...

Called constraints: Called.Never, Called.Once, Called.AtLeastOnce, Called.Exactly(n), Called.AtLeast(n), Called.AtMost(n)


Argument Capture

// Single parameter - LastArg
var getTracking = stub.GetUser.Call((id) => new User { Id = id });
service.GetUser(42);
Assert.Equal(42, getTracking.LastArg);

// Multiple parameters - LastArgs tuple
var updateTracking = stub.Update.Call((int id, string name) => { });
service.Update(1, "Alice");
var (id, name) = updateTracking.LastArgs;

Strict Mode

Throws StubException for unconfigured member access:

// Per-stub
// [KnockOff(Strict = true)]
// public partial class StrictStub : IService { }

// Or at runtime
var stub = new SvcStub();
stub.Strict();

// Assembly-wide default
// [assembly: KnockOffStrict]

Stub Overrides

Load stub-overrides.md when creating or modifying any KnockOff stubs. It covers the recommended approach: standalone stubs with constructor parameters and protected override properties/methods (underscore suffix: UserId_, GetById_). Custom constructors must chain to this(). Return()/Call()/Get()/Set() supersede overrides per-test. Standalone patterns only (1-4).


Source Delegation (Interface Stubs Only)

stub.Source(realImpl) delegates unconfigured calls to a real implementation. See source-delegation.md for hierarchy support and details.

var stub = new SkSourceDelegationStub();
stub.Source(realImplementation);

// Configured members override source
stub.GetById.Call((id) => testUser);  // This wins over source

// Reset clears tracking (counts, args, sequence position) and source delegation
// but preserves callbacks (Return, Returns, Get, Set)
// stub.GetById.Reset();

Moq Migration Quick Reference

Moq KnockOff
new Mock<IFoo>() new FooStub() or new Stubs.IFoo()
mock.Object stub (interface) or stub.Object (class)
.Setup(x => x.Method()).Returns(val) stub.Method.Return(val)
.Setup(x => x.Method(arg)).Returns(val) stub.Method.When(arg).Return(val)
.Setup(x => x.Prop).Returns(val) stub.Prop.Get(val)
.ReturnsAsync(val) stub.Method.Return(val) (auto-wraps)
.Callback(action) Logic inside Call callback
mock.CallBase = true Default for class stubs
.Verify(x => x.Method(), Times.Once) stub.Method.Verify(Called.Once)
.Verifiable() + mock.Verify() .Verifiable() + stub.Verify()
It.IsAny<T>() Callback always receives all args
It.Is<T>(pred) stub.Method.When(pred).Return(val)

Common Mistakes

Missing partial Keyword

// WRONG: Compilation errors
// [KnockOff]
// public class FooStub : IFoo { }

// RIGHT:
[KnockOff]
public partial class SkillPartialDemoStub : ISvc { }

Wrong Callback Signature

// WRONG: Type mismatch
// stub.Process.Call((string id) => { });  // Method takes int

// RIGHT: Match signature exactly
stub.Process.Call((int id) => { });

Forgetting .Object for Class Stubs

// WRONG:
// MyClass service = stub;  // Won't compile

// RIGHT:
var stub = new Stubs.ServiceBase();
ServiceBase service = stub.Object;

Using Func<>/Action<> Instead of Named Delegates

// WRONG: KnockOff doesn't support generic delegates
// [KnockOff<Func<int, string>>]  // Won't work

// RIGHT: Define a named delegate
public delegate string SkillNamedOperation(int value);
[KnockOff<SkillNamedOperation>]
public partial class SkillNamedDelegateHost { }

Reference Documentation

Load these on demand for detailed coverage of specific topics:

Member Types

  • references/methods.md — Method configuration, ref/out params, overloads, argument capture
  • references/properties.md — Property Get/Set, LastSetValue, decision guide
  • references/indexers.md — Per-key builders, all-keys callbacks, multi-param indexers, priority chain
  • references/events.md — Raise() signatures, HasSubscribers, VerifyAdd/VerifyRemove
  • references/delegates.md — Named delegate stubs, Interceptor access, implicit conversion

Cross-Cutting Features

  • references/sequences.md — Method/property/indexer sequences, ThenReturn, ThenDefault, exhaustion
  • references/when-chains.md — Value matching, predicate matching, ThenWhen, first-match-wins
  • references/verification.md — Verify/Verifiable/VerifyAll, Called constraints, VerificationException
  • references/async-methods.md — Three-tier auto-wrapping for Task/ValueTask
  • references/generic-methods.md — Of() pattern, CalledTypeArguments, multi-type params

Advanced Features

  • references/stub-overrides.md — Protected override methods/properties, underscore convention
  • references/source-delegation.md — Interface hierarchy, partial stubbing, priority chain
  • references/strict-mode.md — Per-stub, runtime, assembly-wide strict configuration

Guides

  • references/patterns.md — Complete guide to all 9 stub patterns
  • references/moq-migration.md — Comprehensive Moq-to-KnockOff migration guide
Install via CLI
npx skills add https://github.com/NeatooDotNet/KnockOff --skill knockoff
Repository Details
star Stars 2
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator
NeatooDotNet
NeatooDotNet Explore all skills →