Generics¶
Imposter supports generic methods, interfaces, classes, and fully open generic targets. Each closed generic combination gets its own setups and call history.
Note
Generic support applies across methods, properties, indexers, and events. Verification and argument matching are always scoped to the concrete type arguments used at call site.
Generic Methods¶
Generic methods (with their own <T> parameters) are treated like independent overloads per closed type argument combination. Each set of type arguments gets its own setup and verification.
Example
using Imposter.Abstractions;
using Imposter.Tests.Features.OpenGenericImposter;
[assembly: GenerateImposter(typeof(IHaveGenericMethods))]
namespace Imposter.Tests.Features.OpenGenericImposter
{
public interface IHaveGenericMethods
{
TResult GetValue<TResult>();
}
}
Example
var imposter = IHaveGenericMethods.Imposter();
imposter.GetValue<int>().Returns(5);
imposter.GetValue<string>().Returns("value");
var service = imposter.Instance();
service.GetValue<int>().ShouldBe(5);
service.GetValue<string>().ShouldBe("value");
var imposter = new IHaveGenericMethodsImposter();
imposter.GetValue<int>().Returns(5);
imposter.GetValue<string>().Returns("value");
var service = imposter.Instance();
service.GetValue<int>().ShouldBe(5);
service.GetValue<string>().ShouldBe("value");
Generic types¶
Generic interfaces like IOpenGenericMethodTarget<T> get a distinct imposter per closed type argument. Each closed generic interface tracks its own setups, calls, and verification.
Example
using Imposter.Abstractions;
using Imposter.Tests.Features.OpenGenericImposter;
[assembly: GenerateImposter(typeof(IOpenGenericMethodTarget<>))]
namespace Imposter.Tests.Features.OpenGenericImposter
{
public interface IOpenGenericMethodTarget<T>
{
T GetNext();
}
}
Example
var imposter = IOpenGenericMethodTarget<string>.Imposter();
imposter.GetNext().Returns("alpha").Then().Returns("beta");
var service = imposter.Instance();
service.GetNext().ShouldBe("alpha");
service.GetNext().ShouldBe("beta");
var imposter = new IOpenGenericMethodTargetImposter<string>();
imposter.GetNext().Returns("alpha").Then().Returns("beta");
var service = imposter.Instance();
service.GetNext().ShouldBe("alpha");
service.GetNext().ShouldBe("beta");
Type Matching¶
- For input parameters, setups declared on a generic argument type (like
Animal) match calls made with either that type itself or any derived type (likeCat);
Example
public void GivenInputParameterSetup_WhenInvokedWithDerivedOrSameType_ShouldInvoke()
{
var capturedAnimals = new List<IAnimal>();
_sut.GenericSingleParam<Animal>(Arg<Animal>.Any())
.Callback(animal =>
{
capturedAnimals.Add(animal);
});
var animal = new Animal("mittens");
_sut.Instance().GenericSingleParam(animal);
var cat = new Cat("mittens");
_sut.Instance().GenericSingleParam(cat);
capturedAnimals.Count.ShouldBe(2);
capturedAnimals[0].ShouldBe(animal);
capturedAnimals[1].ShouldBe(cat);
}
Example
public void GivenInputParameterSetup_WhenInvokedWithBase_ShouldNotInvoke()
{
Cat? capturedAnimal = null;
_sut.GenericSingleParam<Cat>(Arg<Cat>.Any())
.Callback(animal =>
{
capturedAnimal = animal;
});
var animal = new Animal("mittens");
_sut.Instance().GenericSingleParam(animal);
capturedAnimal.ShouldBeNull();
}
- For output parameters, setups declared on a derived generic argument type (like
Cat) can be invoked even when the method is called using a base type (likeIAnimal) as the output parameter; the provided value is assigned and observed through the base-typed variable.
Example
public void GivenOutputParameterSetupOnDerivedType_WhenMethodIsInvokedWithBaseType_ShouldInvoke()
{
Cat? providedCat = null;
_sut.GenericSingleOutParam<Cat>(OutArg<Cat>.Any())
.Callback((out Cat value) =>
{
providedCat = new Cat("mittens");
value = providedCat;
});
_sut.Instance().GenericSingleOutParam<IAnimal>(out var impersonatedAnimal);
providedCat.ShouldNotBeNull();
impersonatedAnimal.ShouldBe(providedCat);
}
Example
public void GivenOutputParameterSetupOnBase_WhenMethodIsInvokedWithDerivedType_ShouldNotInvoke()
{
var baseCallbackInvoked = false;
_sut.GenericSingleOutParam<IAnimal>(OutArg<IAnimal>.Any())
.Callback((out IAnimal value) =>
{
baseCallbackInvoked = true;
value = new Animal("base");
});
_sut.Instance().GenericSingleOutParam<Cat>(out Cat? cat);
baseCallbackInvoked.ShouldBeFalse();
cat.ShouldBeNull();
}
- For ref parameters, setups are matched by the exact static generic argument: a setup declared on a base type (like
IAnimal) does not match when the method is invoked with arefargument of a derived type (likeCat), but a setup declared onDogmatches aref Dogargument.
Example
public void GivenRefParameterSetupOnBase_WhenInvokedWithDerivedType_ShouldNotInvoke()
{
var animalCallback = false;
_sut.GenericSingleRefParam<IAnimal>(Arg<IAnimal>.Any())
.Callback((ref IAnimal _) => animalCallback = true);
var cat = new Cat("mittens");
_sut.Instance().GenericSingleRefParam(ref cat);
animalCallback.ShouldBeFalse();
}
Example
public void GivenRefParameterSetup_WhenInvokedWithSameType_ShouldInvoke()
{
var dogCallbackInvoked = false;
_sut.GenericSingleRefParam<Dog>(Arg<Dog>.Any())
.Callback((ref Dog _) => dogCallbackInvoked = true);
var dog = new Dog("buddy");
_sut.Instance().GenericSingleRefParam(ref dog);
dogCallbackInvoked.ShouldBeTrue();
}
- For generic return types, setups declared for a more specific derived type (like
Cat) can be used when the method is invoked with a base return type (likeIAnimal), but setups declared for a base type (likeIAnimal) are not used when the method is invoked with a more specific derived return type (likeCat), so the call returns the default value instead.
Example
public void GivenGenericReturnTypeSetupForDerived_WhenInvokedAsBase_ShouldReturnDerivedInstance()
{
var providedCat = new Cat("mittens");
_sut.GenericReturnType<Cat>().Returns(providedCat);
var impersonatedAnimal = _sut.Instance().GenericReturnType<IAnimal>();
impersonatedAnimal.ShouldBe(providedCat);
}
Example
public void GivenGenericReturnTypeSetupForBase_WhenInvokedAsDerived_ShouldReturnDefault()
{
_sut.GenericReturnType<IAnimal>().Returns(new Animal("base"));
var result = _sut.Instance().GenericReturnType<Cat>();
result.ShouldBeNull();
}
Type parameters that allow ref structs¶
A method's type parameter can allow ref structs (where T : allows ref struct, C# 13). A value of it may be a ref struct, which an imposter can't keep or match, so the imposter passes it through like a ref struct parameter, whatever the type argument:
- Setups and verification take matchers for the method's other parameters only. The delegates passed to
Returns,CallbackandThrows, and the base implementation, get the value. - A result of that type comes only from a
Returnsdelegate or the base implementation, and the invocation history leaves it out. - A setup applies only to calls with the same type arguments. The type matching above doesn't apply to such a method.
Example
// int Measure<T>(T value, int scale) where T : allows ref struct, where Gauge is a ref struct
imposter.Measure<Gauge>(Arg<int>.Any()).Returns((value, scale) => value.Level * scale);
var measured = imposter.Instance().Measure(new Gauge(2), 3); // 6
Note
The imposter repeats the anti-constraint, which needs the Roslyn 4.14 compiler or later, as in the .NET 9.0.300 SDK and Visual Studio 17.14. An older compiler reports IMP009 for such a method.
The imposter of a generic target keeps values of the target's own type parameters, so it leaves out their anti-constraint: for a target's type parameter that allows ref structs, the imposter takes only type arguments that aren't ref structs.
Targets nested in generic types¶
An open nested target's imposter takes the containing types' type parameters from outer to inner, followed by the target's own parameters. Their constraints apply to the imposter too. For example, Outer<T>.Inner<U> generates InnerImposter<T, U>:
using Imposter.Abstractions;
using Imposter.Tests.Features.OpenGenericImposter.NestedTargets;
[assembly: GenerateImposter(typeof(Outer<>.Inner<>))]
namespace Imposter.Tests.Features.OpenGenericImposter.NestedTargets
{
public class Outer<T>
where T : class
{
public interface Inner<U>
where U : T
{
U Get();
}
}
public static class Usage
{
public static Outer<object>.Inner<string> Create()
{
var imposter = new InnerImposter<object, string>();
imposter.Get().Returns("value");
return imposter.Instance();
}
}
}
A non-generic target such as Outer<T>.Inner still generates a generic InnerImposter<T>. A closed containing type stays closed: registering Outer<int>.Inner generates InnerImposter, and registering Outer<int>.Inner<string> generates InnerImposter<U> for Outer<int>.Inner<U>.
The imposter keeps the nested target's simple name. If two registrations would produce the same imposter name and total number of type parameters, IMP007 reports the collision. Register one with putInTheSameNamespace: false to place it in a namespace derived from its full containing-type chain.
Renaming a type parameter to avoid a collision does not change the imposter's namespace or file name.
Open Generics¶
Open generics let you register a generic interface or class once (for example, typeof(IAsyncObservable<>)) and then create imposters for any concrete type you close it with at call site.
Note
Register the open type. A closed registration such as typeof(IAsyncObservable<int>) produces the same generic imposter, ignores its type arguments, and reports warning IMP006.
From a behaviour point of view:
- Each closed generic target gets its own imposter type and its own call tracking. An
IAsyncObservable<string>imposter and anIAsyncObservable<int>imposter never share setups or call history. - Verification is always scoped to both the concrete type argument and the imposter instance.
stringImposter.OnNext(Arg<string>.Any()).Called(...)only counts calls made through instances created fromstringImposter; calls made throughintImposter.Instance()do not contribute. - This isolation applies across all members on the open generic type: methods, properties, indexers, and events on
IAsyncObservable<string>are independent from the same members onIAsyncObservable<int>(or any otherT). - If you only invoke
OnNexton theintstream and then try to verifyOnNexton thestringimposter, verification will fail because there are no matching calls for that closed type.
Example
using Imposter.Abstractions;
using Imposter.Tests.Features.OpenGenericImposter;
[assembly: GenerateImposter(typeof(IAsyncObservable<>))]
namespace Imposter.Tests.Features.OpenGenericImposter
{
public interface IAsyncObservable<T>
{
void OnNext(T item);
}
}
Example
var stringImposter = IAsyncObservable<string>.Imposter();
var intImposter = IAsyncObservable<int>.Imposter();
var stringStream = stringImposter.Instance();
var intStream = intImposter.Instance();
stringStream.OnNext("payload");
stringStream.OnNext("another");
intStream.OnNext(42);
stringImposter.OnNext(Arg<string>.Any()).Called(Count.Exactly(2));
intImposter.OnNext(Arg<int>.Any()).Called(Count.Once());
var stringImposter = new IAsyncObservableImposter<string>();
var intImposter = new IAsyncObservableImposter<int>();
var stringStream = stringImposter.Instance();
var intStream = intImposter.Instance();
stringStream.OnNext("payload");
stringStream.OnNext("another");
intStream.OnNext(42);
stringImposter.OnNext(Arg<string>.Any()).Called(Count.Exactly(2));
intImposter.OnNext(Arg<int>.Any()).Called(Count.Once());