使用虚拟方法模拟具体 class

Mocking a concrete class with a virtual method

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

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

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

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

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

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

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

由于 NSubstitute 没有来自其他流行模拟框架的 Mock<T>Fake<T> 的等价物,AutoFixture 必须提供一个特殊的样本构建器,称为 SubstituteRelay,以填充差距。您可以像使用任何其他样本生成器一样使用此 class 来指示 Fixture 在请求特定类型实例时 return 进行模拟。

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,您可以创建自己的扩展方法,抽象出注册中继和冻结实例的两行代码。