| 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);
calc.Add(0, 0);
calc.Add(0, 0);
stub.Add.Return(1, 999).ThenDefault();
calc.Add(0, 0);
calc.Add(0, 0);
calc.Add(0, 0);
2. Events Use Raise() and Bare Names
stub.Started.Raise(stub, EventArgs.Empty);
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).
var stub = new Stubs.ServiceBase();
ServiceBase service = stub.Object;
service.Initialize();
4. Closed Generic Stubs Use Simple Names
var stub = new Stubs.IRepository();
5. Called.Between() Does NOT Exist
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) => { });
service.Name = "test";
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" });
stub.GetUser.Call((id) => new User { Id = id, Name = $"User{id}" });
stub.Save.Call((user) => { });
stub.GetUserAsync.Call((id) => new User { Id = id });
stub.SaveAsync.Call((user) => { });
stub.GetNext.Return(1, 2, 3);
stub.Add.Call((int a, int b) => a + b).ThenReturn(100, 200);
stub.GetNext.Return(1, 2).ThenDefault();
stub.GetUser.When(42).Return(adminUser);
stub.GetUser.When(1).Return(regularUser);
stub.GetUser.When(id => id < 0).Return(null);
stub.GetUser
.When(42).Return(adminUser)
.ThenWhen(id => id > 100).Return(premiumUser)
.ThenWhen(id => id > 0).Return(regularUser);
stub.Log.When("error").Call((msg) => { });
Property Configuration
stub.Name.Get("TestName");
stub.Timestamp.Get(() => DateTime.UtcNow);
stub.Name.Set((value) => capturedValues.Add(value));
stub.Counter.Get(() => 1).ThenGet(() => 2).ThenGet(() => 3);
Indexer Configuration
stub.Indexer["key1"].Returns("value1");
stub.Indexer["key2"].Returns("value2");
stub.Indexer.Get((key) => $"computed-{key}");
stub.Indexer.Set((key, value) => { });
stub.Indexer.When(key => key.StartsWith("prefix_", StringComparison.Ordinal)).Returns("matched");
Event Configuration
stub.DataReceived.Raise(stub, new DataEventArgs("test-data"));
stub.DataReceived.VerifyAdd(Called.Once);
stub.DataReceived.VerifyRemove(Called.Never);
Generic Methods
stub.GetById.Of<User>().Call((id) => new User { Id = id });
stub.GetById.Of<Product>().Call((id) => new Product { Id = id });
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();
stub.Interceptor.Return(42);
stub.Interceptor.Call((a, b) => a + b);
stub.Interceptor.Return(10, 20, 30);
stub.Interceptor.When(1, 2).Return(100)
.ThenWhen(3, 4).Return(200);
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);
stub.Strict = true;
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);
Called constraints: Called.Never, Called.Once, Called.AtLeastOnce, Called.Exactly(n), Called.AtLeast(n), Called.AtMost(n)
Argument Capture
var getTracking = stub.GetUser.Call((id) => new User { Id = id });
service.GetUser(42);
Assert.Equal(42, getTracking.LastArg);
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:
var stub = new SvcStub();
stub.Strict();
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);
stub.GetById.Call((id) => testUser);
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
[KnockOff]
public partial class SkillPartialDemoStub : ISvc { }
Wrong Callback Signature
stub.Process.Call((int id) => { });
Forgetting .Object for Class Stubs
var stub = new Stubs.ServiceBase();
ServiceBase service = stub.Object;
Using Func<>/Action<> Instead of Named Delegates
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