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.
CRITICAL GOTCHAS
1. Sequences REPEAT Last Value After Exhaustion
<!-- snippet: skill-gotcha-sequence-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
<!-- snippet: skill-gotcha-event-raise -->// 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
<!-- snippet: skill-gotcha-closed-generic -->// For [KnockOff<IRepository<User>>]: var stub = new Stubs.IRepository(); // NOT Stubs.IRepository<User>
5. Called.Between() Does NOT Exist
<!-- snippet: skill-gotcha-times-between -->// 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
<!-- snippet: skill-gotcha-onset-no-auto-update -->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
<!-- snippet: skill-standalone-pattern -->[KnockOff]
public partial class SkillUserRepoStub : ISkillUserRepo { }
[Fact]
public void StandaloneStub_ConfigureAndVerify()
{
var stub = new SkillUserRepoStub();
stub.GetById.Return((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
<!-- snippet: skill-inline-interface-pattern -->[KnockOff<ISkillEmailService>]
public partial class SkillEmailTests
{
[Fact]
public void Test()
{
var stub = new Stubs.ISkillEmailService();
stub.Send.Return((to, subj) => 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
<!-- snippet: skill-method-returns -->stub.GetUser.Return(new User { Id = 1, Name = "Alice" });
// With arguments
stub.GetUser.Return((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.Return((id) => new User { Id = id }); // Returns Task<User>
stub.SaveAsync.Return((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.Return((a, 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
<!-- snippet: skill-property-config -->// 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
<!-- snippet: skill-indexer-config -->// 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
<!-- snippet: skill-event-config -->// 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
<!-- snippet: skill-generic-methods -->// Use .Of<T>() for type-specific configuration
stub.GetById.Of<User>().Return((id) => new User { Id = id });
stub.GetById.Of<Product>().Return((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.Return((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.Return((int x) => x * 2); // simplified, auto-wrapped
// Verification (fresh stub for clean tracking)
var verifyStub = new Stubs.SkillArithmeticOp();
verifyStub.Interceptor.Return((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.Return((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
<!-- snippet: skill-arg-capture -->// Single parameter - LastArg
var getTracking = stub.GetUser.Return((id) => new User { Id = id });
service.GetUser(42);
Assert.Equal(42, getTracking.LastArg);
// Multiple parameters - LastArgs tuple
var updateTracking = stub.Update.Call((id, 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 (Standalone Only)
Override protected virtual methods/properties with underscore suffix for reusable defaults. Return()/Call()/Get()/Set() supersede overrides per-test. See stub-overrides.md for full reference.
[KnockOff]
public partial class SkStubOverrideRepoStub : IUserRepo { }
public partial class SkStubOverrideRepoStub
{
// Override virtual method with underscore suffix - compiler enforces signature!
protected override User? GetById_(int id) => new User { Id = id, Name = "Default" };
}
Return() supersedes the override: stub.GetById.Return(id => new User { Id = id, Name = "Override" });
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.Return((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 Return/Call callback |
mock.CallBase = true | Default for class stubs |
.Verify(x => x.Method(), Times.Once) | tracking.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
<!-- snippet: skill-mistake-partial -->
// WRONG: Compilation errors
// [KnockOff]
// public class FooStub : IFoo { }
// RIGHT:
[KnockOff]
public partial class SkillPartialDemoStub : ISvc { }
Wrong Callback Signature
<!-- snippet: skill-mistake-wrong-signature -->// WRONG: Type mismatch
// stub.Process.Return((string id) => { }); // Method takes int
// RIGHT: Match signature exactly
stub.Process.Call((int id) => { });
Forgetting .Object for Class Stubs
<!-- snippet: skill-mistake-forgetting-object -->// WRONG: // MyClass service = stub; // Won't compile // RIGHT: var stub = new Stubs.ServiceBase(); ServiceBase service = stub.Object;
Using Func<>/Action<> Instead of Named Delegates
<!-- snippet: skill-mistake-func-action -->// 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<T>/ValueTask<T> - •
references/generic-methods.md— Of<T>() 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