Skip to content

Latest commit

 

History

History
198 lines (139 loc) · 6.73 KB

File metadata and controls

198 lines (139 loc) · 6.73 KB

Home / Guides / Generic Methods

Working with 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(...).


The Challenge

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 GetById called with any type?)

Type-Specific Configuration

Use .Of<T>() to access the type-specific interceptor, then call Return to configure behavior for that type argument.

Return Signature and Return Value

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: IMethodTracking object 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 });

Type-Specific Verification

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);

Multiple Type Parameters

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());

Inspecting Called Type Arguments

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.


Resetting State

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);

Complete Example

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 });

Key Takeaways

  • .Of<T>() provides type-specific access to the interceptor for a specific type argument
  • Return configures the callback for that type—parameters match the method signature, return type matches with type arguments substituted
  • Return returns IMethodTracking enabling 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 CalledTypeArguments shows 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