AutoFixture AutoMoq - SetReturnsDefault() 不适用于夹具创建的模拟

AutoFixture AutoMoq - SetReturnsDefault() does not work with fixture created mocks

我有一个模拟。这个 mock 有两个方法,MethodA()MethodB()。我想将这两种方法设置为 return false。我创建了各种版本的代码,它们都应该可以工作,但有些不能:

这些作品:

1.

var mock = fixture.Freeze<Mock<MyInterface>>();
mock
    .Setup(m => m.MethodA(It.IsAny<T>(), It.IsAny<T>()))
    .ReturnsAsync(false);
mock
    .Setup(m => m.MethodB(It.IsAny<T>(), It.IsAny<T>()))
    .ReturnsAsync(false);

var sut = fixture.Create<MySut>();
sut.Do(); // Calls MethodA() and MethodB() - Both return false, works

2.

var mock = new Mock<MyInterface>();
mock.SetReturnsDefault(Task.FromResult(false));
fixture.Inject(mock);

var sut = fixture.Create<MySut>();
sut.Do(); // Calls MethodA() and MethodB() - Both return false, works

3.

var mock = new Mock<MyInterface>();
mock.SetReturnsDefault(Task.FromResult(false));
fixture.Inject(mock.Object);

var sut = fixture.Create<MySut>();
sut.Do(); // Calls MethodA() and MethodB() - Both return false, works

这些不是:

4.

var mock = fixture.Freeze<Mock<MyInterface>>();
mock.SetReturnsDefault(Task.FromResult(false));

var sut = fixture.Create<MySut>();
sut.Do(); // Calls MethodA() and MethodB() - MethodA returns true, fails

5.

var mock = fixture.Create<Mock<MyInterface>>();
mock.SetReturnsDefault(Task.FromResult(false));
fixture.Inject(mock);

var sut = fixture.Create<MySut>();
sut.Do(); // Calls MethodA() and MethodB() - MethodA returns true, fails

6.

var mock = fixture.Create<Mock<MyInterface>>();
mock.SetReturnsDefault(Task.FromResult(false));
fixture.Inject(mock.Object);

var sut = fixture.Create<MySut>();
sut.Do(); // Calls MethodA() and MethodB() - MethodA returns true, fails

根据结果,罪魁祸首似乎是 Fixture.Create() 方法*。出于某种原因,如果模拟是使用 fixture.Create() 而不是 new 关键字创建的,它将不会保留我设置的配置使用 SetReturnsDefault(),即使模拟被冻结(意味着 Fixture.Inject() 被调用)。有人可以解释为什么吗?


脚注:

* Fixture.Create() 在调用 Fixture.Freeze() 时也会在内部调用 - 冻结是只是 shorthand 调用 Fixture.Create() 然后 Fixture.Inject()

因此,这两个片段是等价的:

var mock = fixture.Freeze<Mock<MyInterface>>();

-

var mock = fixture.Create<Mock<MyInterface>>();
fixture.Inject(mock);

由于您分享了您正在使用 AutoMoqCustomization,它解释了您看到的行为,尽管这是由于 MoqAutoFixture 中的一些内部机制以某种方式交互你可能想不到。

为了完整起见,这里是您在问题中提到的 class 和界面的删减版本:

public interface IMyInterface
{
    Task<bool> MethodA();

    Task<bool> MethodB();
}

public class Sut
{
    private readonly IMyInterface dep;

    public Sut(IMyInterface dep)
    {
        this.dep = dep;
    }

    public async Task<bool> Do()
    {
        var one = await dep.MethodA();
        var two = await dep.MethodB();

        return one || two;
    }
}

下面是一个我们可以用来说明两种行为相互作用的测试:

[Test]
public async Task FixtureNoConfigureMembers()
{
    var fixture = new Fixture().Customize(new AutoMoqCustomization() { ConfigureMembers = false });

    var mock = fixture.Freeze<Mock<IMyInterface>>();

    mock.SetReturnsDefault(Task.FromResult(false));

    var sut = fixture.Create<Sut>();
    var result = await sut.Do();
    Assert.False(result);
}

要考虑的两个重要实施细节是:

  • 当您在自定义中指定 ConfigureMembers = true 时 AutoFixture 会做什么。
  • Moq 如何确定何时使用 SetReturnsDefault
  • 设置的默认值

由于这两个项目都是开源的,因此很容易深入了解。我不会在这里粘贴每个人 class,但如果您需要更多详细信息,可以查看 Autofixture's source and Moq's source

Moq 的 SetReturnsDefault 具有相当直接的行为:

  • 如果设置匹配,则使用该设置。默认 return 值被忽略。这是重要的一点。
  • 如果没有匹配的设置,并且 return 值的类型正确,则默认值为 returned。

在自动夹具方面...

AutoMoqCustomization 这样,当请求类型为 Mock<T> 的样本时,AutoFixture 将首先通过本质上调用 new Mock<T>() 来创建 Mock(它比这稍微微妙一些,但本质上就是这个界面的情况。)

创建mock后,如果ConfigureMembers为真,Autofixture将另外枚举所有mock类型的虚方法,相当于

mock.Setup(m => m.MethodName(It.IsAny<T>... for all arguments... ))
    .Returns(fixture.Create<TReturn>())

这会覆盖 AutoMoqCustomization 的默认行为,即允许 Moq 处理默认值选择(通常通过让它创建自己的模拟)。

您大概可以看出它的发展方向。因为 AutoFixture 已经为所有虚拟方法创建了设置,所以这完全覆盖了 Moq 提供的 SetReturnsDefault 行为。

请注意,此行为是因为 Moq 过去没有 SetReturnsDefault 方法——因此 AutoFixture 无法利用 DefaultValueProvider 来注入它创建的样本。有一个 PR on AutoFixture 可以更改行为以利用 Moq 提供的 DefaultValueProvider 属性。据推测,这将恢复 SetReturnsDefault 覆盖特定类型行为的能力。 (虽然我根本没有深入探索 PR)