Home / Guides / Generic Methods
Generic methods present unique challenges when stubbing. Unlike non-generic methods where you configure a single behavior, generic methods can be called with different type arguments—each potentially requiring different configuration and verification.
KnockOff solves this with the .Of<T>() accessor pattern, giving you type-specific control while maintaining aggregate tracking across all type arguments.
Critical concept: Use .Of<T>() to access type-specific configuration and verification for generic methods. Base properties like CalledTypeArguments track calls across all type arguments.
Configuration and verification: The .Of<T>().Return() method configures the callback for a specific type argument and returns an IMethodTracking object for verification. Use tracking.Verify(Called) to verify call counts. Each type argument has independent configuration—configuring .Of<User>().Return(...) does not affect .Of<Order>().Return(...).
Consider a generic repository method:
public interface IRepository
{
T? GetById<T>(int id) where T : class, new();
}In tests, you might call GetById<User>(1) and GetById<Order>(2). These are the same method but with different type arguments. You need to:
- Configure different return values for each type
- Verify calls per type (how many times was
GetById<User>called?) - Track aggregate calls (how many times was
GetByIdcalled with any type?)
Use .Of<T>() to access the type-specific interceptor, then call Return to configure behavior for that type argument.
The Return method accepts a callback matching the method signature and returns IMethodTracking for verification:
- Callback parameters: Match the original method parameters
- Callback return type: Matches the method's return type with the specific type argument substituted
- Return value:
IMethodTrackingobject providing.Verify(Called)(see Verification Guide)
Key point: Return is type-specific—each type argument needs its own configuration. The returned IMethodTracking object is used to verify calls for that specific type argument.
// Configure behavior for User type
stub.GetById.Of<User>().Call((id) =>
new User { Id = id, Name = "Test User" });You can configure multiple types independently. Each Return is specific to its type argument:
// Configure different behavior for each type
stub.GetById.Of<User>().Call((id) =>
new User { Id = id, Name = "User" });
stub.GetById.Of<Order>().Call((id) =>
new Order { Id = id, Amount = 99.99m });After execution, verify calls per type using the same .Of<T>() accessor. The Return method returns an IMethodTracking object that provides verification capabilities.
// Verify calls for specific type using Times
tracking.Verify(Called.Exactly(2));You can verify calls for multiple types independently:
// Verify each type was called independently
userTracking.Verify(Called.Exactly(2));
orderTracking.Verify(Called.Once);For methods with multiple type parameters, use .Of<T1, T2, ...>():
// Configure for string -> int conversion
stub.Convert.Of<string, int>().Call((source) =>
int.Parse(source));
// Configure for int -> string conversion
stub.Convert.Of<int, string>().Call((source) =>
source.ToString());Use CalledTypeArguments to see which type combinations were actually invoked:
// CalledTypeArguments contains all types used
var types = stub.GetById.CalledTypeArguments;This is particularly useful when verifying that specific types were or were not used during test execution.
Reset type-specific state using .Of<T>().Reset():
// Reset only User-specific state
stub.GetById.Of<User>().Reset();
stub.GetById.Of<User>().Verify(Called.Never);
stub.GetById.Of<Order>().Verify(Called.Once);To reset all type arguments at once, call Reset() on the base interceptor:
// Reset all type-specific state
stub.GetById.Reset();
stub.GetById.Of<User>().Verify(Called.Never);
stub.GetById.Of<Order>().Verify(Called.Never);Here's a full test demonstrating generic method stubbing for a serializer/deserializer:
// Configure Serialize for different types
var serializeUserTracking = stub.Serialize.Of<User>().Call((obj) =>
$"{{\"Id\":{obj.Id},\"Name\":\"{obj.Name}\"}}");
var serializeOrderTracking = stub.Serialize.Of<Order>().Call((obj) =>
$"{{\"Id\":{obj.Id},\"Amount\":{obj.Amount}}}");
// Configure Deserialize
var deserializeUserTracking = stub.Deserialize.Of<User>().Call((data) =>
new User { Id = 1, Name = "Deserialized User" });
var deserializeOrderTracking = stub.Deserialize.Of<Order>().Call((data) =>
new Order { Id = 2, Amount = 50.00m });.Of<T>()provides type-specific access to the interceptor for a specific type argumentReturnconfigures the callback for that type—parameters match the method signature, return type matches with type arguments substitutedReturnreturnsIMethodTrackingenabling verification via.Verify(Called)- Base properties (
CalledTypeArguments,Reset()) track and manage calls across all types - Multiple type parameters use
.Of<T1, T2, ...>()matching the method signature - Verification uses
tracking.Verify(Called)for type-specific call count assertions - Reset behavior differs:
.Of<T>().Reset()is type-specific,.Reset()clears everything - Type discovery via
CalledTypeArgumentsshows which types were actually used
Generic methods work seamlessly with all KnockOff patterns—Stand-Alone, Inline Interface, and Inline Class. The .Of<T>() API remains consistent regardless of how you declare your stub.
Next: Advanced Callbacks for complex callback scenarios and state management.
UPDATED: 2026-02-18