首页 > 解决方案 > 用虚方法模拟一个具体的类

问题描述

我想用虚拟方法测试一个依赖于另一个类的类。

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

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

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

现在我想HostClass用 Autofixture + NSubstitute 来测试一下。我的期望:

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

事实上,当我这样做时,正在调用Freeze().Get().Returns()真正的方法。Get如何自定义 Autofixture 来模拟所有虚拟方法?

最好不要讨论接口与虚拟方法等

更新 这不起作用:

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

同时,这有效:

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

除了答案 另一种可能不适合您需求的方法:就我而言,我希望只有直接依赖项才能具有虚拟方法。将模拟较低级别的所有依赖项。

结果,我决定以更具体的方式进行。下面的代码是一个示例,因此您可能需要在将其用于解决方案之前对其进行修改。

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

标签: c#nunitnsubstituteautofixture

解决方案


因为DepClass既不是接口也不是抽象类型,默认情况下,AutoFixture 不会依赖模拟框架来创建实例,而是使用类型中的实际构造函数。

由于 NSubstitute 没有其他流行的模拟框架的等效物Mock<T>Fake<T>来自其他流行的模拟框架,因此 AutoFixture 必须提供一个特殊的样本构建器,称为SubstituteRelay,以填补空白。您可以将此类用作任何其他样本构建器,以指示夹具在请求特定类型实例时返回模拟。

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

这个结构有点长,所以你可以创建一个通用的中继来缩短语法。

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

您应该能够编写类似的测试。

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

为了更接近您的预期 API,您可以创建自己的扩展方法,抽象出注册中继和冻结实例的两行代码。


推荐阅读