Search code examples
c#nunitnsubstituteautofixture

Mocking a concrete class with a virtual method


I want to test a class that depends on another class with the virtual method.

class DepClass
{
  public virtual string Get() => "";
}

class HostClass
{
  private _c;
  public Host(DepClass c){ _c = c; }

  public string Magic() => _c.Get();
}

Now I want to test the HostClass with Autofixture + NSubstitute. My expectation:

Fixture.Freeze<DepClass>().Get().ReturnsForAnyArg("123");
var sut = Fixture.Create<HostClass>();
var res = sut.Magic(); //should be 123

As a fact, when I do Freeze().Get().Returns() the real Get method is being called. How to customize Autofixture to mock all virtual methods?

Would be great not to discuss interface vs virtual methods, etc

Update This does not work:

Fixture.Freeze<DepClass>().Get().ReturnsForAnyArgs("123");

At the same time, this works:

Substitute.For<DepClass>().Get().ReturnsForAnyArgs("123");

In addition to the answer Another approach that might be not fit your needs: in my case, I expect that only direct dependencies can have virtual methods. all dependencies of the lower level will be mocked.

As result, I decided to go in a bit more specific way. The code below is an example, so you might be want to modify it before using it in your solution.

abstract class UnitTestBase<T>
{
  protected IFixture Fixture { get; private set; }
  protected IList<Type> ProxyTypes {get; private set;}
  protected CreateSut(): T => Fixt.Create<T>();

  [SetUp]
  protected virtual Setup()
  {
    ProxyTypes = new List<Type>();
    Fixture = new Fixture().Customize(new CompositeCustomization(
        new AutoNSubstituteCustomization {
            ConfigureMembers = true,
            GenerateDelegates = true
        })
    );

    SetupTypesToProxy();
    Fixture.Customizations.Add(new SubstituteRelay(new ProxyExactTypesSpecification(ProxyTypes)));
  }

  protected virtual void SetupTypesToProxy()
    => typeof(T)
        .GetConstructors(BindingFlags.Public | BindingFlags.Instance)
        .SelectMany(ctorInfo => ctorInfo
            .GetParameters()
            .Select(paramInfo => paramInfo.ParameterType)
            .Where(ShouldProxy))
        .ForEach(t => ProxyTypes.Add(t));

  private static bool ShouldProxy(Type type)
    => !type.GetTypeInfo().IsInterface && !type.GetTypeInfo().IsAbstract;
}

internal class ProxyExactTypesSpecification: IRequestSpecification
{
    public ProxyExactTypesSpecification(
        IEnumerable<Type> types
        )
    {
        _proxyTypes = types ?? Type.EmptyTypes;
    }

    public bool IsSatisfiedBy(
        object request
        )
    {
        Argument.NotNull(request, nameof(request));

        if (request is Type type)
            return _proxyTypes.Contains(type);

        return false;
    }

    private readonly IEnumerable<Type> _proxyTypes;
}

Solution

  • Because DepClass is neither an interface nor an abstract type, by default, AutoFixture will not rely on a mocking framework to create the instance, and will use the actual constructor from the type.

    Since NSubstitute does not have the equivalent of Mock<T> or Fake<T> from other popular mocking frameworks, AutoFixture had to provide a special specimen builder, called SubstituteRelay, to fill in the gap. You can use this class as any other specimen builder to instruct the Fixture to return a mock when a specific type instance is requested.

    var fixture = new Fixture().Customize(new AutoNSubstituteCustomization());
    fixture.Customizations
        .Add(new SubstituteRelay(new ExactTypeSpecification(typeof(DepClass))));
    

    This construction is a bit lengthy so you could create a generic relay to shorten the syntax.

    public class SubstituteRelay<T> : CompositeSpecimenBuilder
    {
        public SubstituteRelay()
            : base(new SubstituteRelay(new ExactTypeSpecification(typeof(T))))
        {
        }
    }
    

    You should be able to write your test something like.

    [Fact]
    public void ReturnsExpectedValue()
    {
        var fixture = new Fixture().Customize(new AutoNSubstituteCustomization());
        fixture.Customizations.Add(new SubstituteRelay<DepClass>());
        fixture.Freeze<DepClass>().Get().ReturnsForAnyArgs("1234");
        var sut = fixture.Create<HostClass>();
    
        var actual = sut.Magic();
    
        Assert.Equal("1234", actual);
    }
    

    To get closer to your intended API, you can create your own extension method that abstracts away the two lines of code that register the relay and freeze the instance.