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