| name | knockoff |
| description | KnockOff source-generated test stubs. Use when creating interface stubs for unit tests, migrating from Moq, understanding the duality pattern (user methods vs callbacks), configuring stub behavior, verifying invocations, or working with interface spy handlers for tracking calls. |
| allowed-tools | Read, Write, Edit, Glob, Grep, Bash(dotnet:*) |
KnockOff - Source-Generated Test Stubs
Overview
KnockOff is a Roslyn Source Generator that creates test stubs for interfaces. Unlike Moq's runtime proxy generation, KnockOff generates compile-time code for type-safe, debuggable stubs.
Key Differentiator: The Duality
KnockOff provides two complementary patterns for customizing stub behavior:
| Pattern | When | Scope | Use Case |
|---|
| User Methods | Compile-time | All tests | Consistent defaults |
| Callbacks | Runtime | Per-test | Test-specific overrides |
[KnockOff]
public partial class SkServiceKnockOff : ISkService
{
protected int GetValue(int id) => id * 2;
}
Priority Order: Callback → User method → Default
Installation
dotnet add package KnockOff
Quick Start
1. Create KnockOff Stub
public interface ISkDataService
{
string Name { get; set; }
string? GetDescription(int id);
int GetCount();
}
[KnockOff]
public partial class SkDataServiceKnockOff : ISkDataService
{
private readonly int _count;
public SkDataServiceKnockOff(int count = 42) => _count = count;
protected int GetCount() => _count;
}
2. Use in Tests
[Fact]
public void Test_DataService()
{
var knockOff = new DataServiceKnockOff(count: 100);
IDataService service = knockOff;
service.Name = "Test";
Assert.Equal("Test", service.Name);
Assert.Equal(1, knockOff.IDataService.Name.SetCount);
var description = service.GetDescription(5);
Assert.Null(description);
Assert.True(knockOff.IDataService.GetDescription.WasCalled);
Assert.Equal(5, knockOff.IDataService.GetDescription.LastCallArg);
Assert.Equal(100, service.GetCount());
}
Interface Spy Properties
Each interface gets its own spy property for tracking and configuration:
[KnockOff]
public partial class SkSpyExampleKnockOff : ISkUserService, ISkPropertyStore, ISkEventSource { }
Multiple Interfaces
When implementing multiple interfaces, each has a separate spy:
[KnockOff]
public partial class SkDataContextKnockOff : ISkRepository, ISkUnitOfWork { }
OnCall API
Callbacks use property assignment with OnCall =:
[KnockOff]
public partial class SkOnCallKnockOff : ISkOnCallService { }
Out/Ref parameters - use explicit delegate type:
[KnockOff]
public partial class SkParserKnockOff : ISkParser { }
Smart Default Return Values
KnockOff returns sensible defaults for unconfigured methods instead of throwing:
| Return Type | Default Value | Example |
|---|
| Value types | default | int → 0, bool → false |
| Nullable refs | null | string? → null |
Types with new() | new T() | List<T> → empty list |
| Collection interfaces | concrete type | IList<T> → new List<T>() |
| Other non-nullable | throws | string, IDisposable |
[KnockOff]
public partial class SkSmartDefaultKnockOff : ISkSmartDefaultService { }
Collection Interface Mapping:
| Interface | Concrete Type |
|---|
IEnumerable<T>, ICollection<T>, IList<T> | List<T> |
IReadOnlyList<T>, IReadOnlyCollection<T> | List<T> |
IDictionary<K,V>, IReadOnlyDictionary<K,V> | Dictionary<K,V> |
ISet<T> | HashSet<T> |
Stub Minimalism
Only stub what the test needs. Don't implement every interface member.
[KnockOff]
public partial class UserServiceKnockOff : IUserService
{
protected User GetUser(int id) => new User { Id = id };
}
Handler Types
| Member Type | Tracking | Callbacks |
|---|
| Method | CallCount, WasCalled, LastCallArg(s), AllCalls | OnCall |
| Property | GetCount, SetCount, LastSetValue | OnGet, OnSet |
| Indexer | GetCount, SetCount, LastGetKey, AllGetKeys, LastSetEntry, AllSetEntries | OnGet, OnSet |
| Event | SubscribeCount, UnsubscribeCount, RaiseCount, WasRaised, LastRaiseArgs, AllRaises | Raise(), Reset(), Clear() |
Reset
knockOff.IService.GetUser.Reset();
Customization Patterns
User Methods (Compile-Time)
Define protected methods matching interface signatures:
[KnockOff]
public partial class SkRepoKnockOff : ISkRepoService
{
protected SkUser? GetById(int id) => new SkUser { Id = id };
protected Task<SkUser?> GetByIdAsync(int id) => Task.FromResult<SkUser?>(new SkUser { Id = id });
}
Rules:
- Must be
protected
- Must match method signature exactly
- Only works for methods (not properties/indexers)
Callbacks (Runtime)
Method Callbacks
[KnockOff]
public partial class SkCallbackMethodKnockOff : ISkCallbackService { }
Property Callbacks
Indexer Callbacks
[KnockOff]
public partial class SkCallbackIndexerKnockOff : ISkCallbackPropertyStore { }
Priority Order
1. Callback (if set) → takes precedence
2. User method (if defined) → fallback for methods
3. Smart default:
- Properties: backing field (initialized via smart defaults)
- Methods: smart default (value types→default, new()→new T(), etc.)
- Indexers: backing dictionary, then smart default
- Void methods: execute silently
Verification Patterns
Call Tracking
[KnockOff]
public partial class SkVerificationKnockOff : ISkVerificationService { }
Property Tracking
Indexer Tracking
[KnockOff]
public partial class SkVerificationIndexerKnockOff : ISkVerificationPropertyStore { }
Backing Storage
Properties
[KnockOff]
public partial class SkBackingServiceKnockOff : ISkBackingService { }
Indexers
[KnockOff]
public partial class SkBackingPropertyStoreKnockOff : ISkBackingPropertyStore { }
Important: Reset() does NOT clear backing storage.
Supported Features
| Feature | Status |
|---|
| Properties (get/set, get-only, set-only) | Supported |
| Void methods | Supported |
| Methods with return values | Supported |
| Methods with parameters | Supported |
| Method overloads (separate handlers) | Supported |
| Out parameters | Supported |
| Ref parameters | Supported |
| Async methods (Task, Task, ValueTask, ValueTask) | Supported |
| Generic interfaces (concrete types) | Supported |
Generic methods (via .Of<T>() pattern) | Supported |
| Multiple interfaces | Supported |
| Interface inheritance | Supported |
| Indexers | Supported |
| Events | Supported |
| Nested classes | Supported |
| User method detection | Supported |
| OnCall/OnGet/OnSet callbacks | Supported |
| Named tuple argument tracking | Supported |
Common Patterns
Conditional Returns
[KnockOff]
public partial class SkPatternServiceKnockOff : ISkPatternService { }
Throwing Exceptions
Sequential Returns
Async Methods
[KnockOff]
public partial class SkAsyncPatternRepositoryKnockOff : ISkAsyncPatternRepository { }
Events
[KnockOff]
public partial class SkEventPatternSourceKnockOff : ISkEventPatternSource { }
Generic Methods
Generic methods use the .Of<T>() pattern for type-specific configuration:
[KnockOff]
public partial class SkGenericSerializerKnockOff : ISkGenericSerializer { }
Method Overloads
When an interface has overloaded methods, each overload gets its own handler with a numeric suffix (1-based):
[KnockOff]
public partial class SkOverloadedServiceKnockOff : ISkOverloadedService { }
Methods without overloads don't get a suffix:
knockOff.IEmailService.SendEmail.CallCount;
Nested Classes
KnockOff stubs can be nested inside test classes:
public partial class SkUserServiceTests
{
[KnockOff]
public partial class SkRepoNestedKnockOff : ISkRepository { }
}
Critical: All containing classes must be partial. This is a C# requirement—the generator produces partial class wrappers that must merge with your declarations.
public class MyTests
{
[KnockOff]
public partial class ServiceKnockOff : IService { }
}
public partial class MyTests
{
[KnockOff]
public partial class ServiceKnockOff : IService { }
}
Works at any nesting depth—just ensure every class in the hierarchy is partial.
Out Parameters
Methods with out parameters are fully supported. Out parameters are outputs, not inputs, so they're excluded from tracking but included in callbacks.
[KnockOff]
public partial class SkOutParamParserKnockOff : ISkOutParamParser { }
Ref Parameters
Methods with ref parameters track the input value (before any callback modification).
[KnockOff]
public partial class SkRefProcessorKnockOff : ISkRefProcessor { }
Moq Migration Quick Reference
| Moq | KnockOff |
|---|
new Mock<IService>() | new ServiceKnockOff() |
mock.Object | Cast or knockOff.AsService() |
.Setup(x => x.Method()) | IService.Method.OnCall = (ko, ...) => ... |
.Returns(value) | OnCall = (ko) => value |
.ReturnsAsync(value) | OnCall = (ko) => Task.FromResult(value) |
.Callback(action) | Logic inside OnCall callback |
.Verify(Times.Once) | Assert.Equal(1, IService.Method.CallCount) |
It.IsAny<T>() | Implicit (callback receives all args) |
It.Is<T>(pred) | Check in callback body |
Additional Resources
For detailed guidance, see:
Skill Sync Status
All code examples in this skill are sourced from compiled, tested samples in the KnockOff repository.
| Repository | Samples Location | Sync Script |
|---|
| KnockOff | src/Tests/KnockOff.Documentation.Samples/Skills/ | scripts/extract-snippets.ps1 |
To update skill files after modifying samples:
.\scripts\extract-snippets.ps1 -Update