Skip to content

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

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 (like IAnimal) 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 a ref argument of a derived type (like Cat), but a setup declared on Dog matches a ref Dog argument.

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 (like IAnimal), but setups declared for a base type (like IAnimal) are not used when the method is invoked with a more specific derived return type (like Cat), 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, Callback and Throws, and the base implementation, get the value.
  • A result of that type comes only from a Returns delegate 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 an IAsyncObservable<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 from stringImposter; calls made through intImposter.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 on IAsyncObservable<int> (or any other T).
  • If you only invoke OnNext on the int stream and then try to verify OnNext on the string imposter, 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());