使用AutoFixture进行依赖项模拟

6

我最近开始使用AutoFixture+AutoMoq,尝试创建一个Func<IDbConnection>实例(即连接工厂)。

var fixture = new Fixture().Customize(new AutoMoqCustomization());
var connectionFactory = fixture.Create<Func<IDbConnection>>();

这似乎相当有效:
1. 我的测试系统可以调用委托,并获得IDbConnection的模拟对象。 2. 然后我可以调用CreateCommand,它将为我提供IDbCommand的模拟对象。 3. 然后我可以调用ExecuteReader,它将为我提供IDataReader的模拟对象。
现在,我想对IDataReader的模拟对象执行其他设置,例如使其在调用Read()时返回true
根据我的阅读,我应该使用Freeze来实现这一点:
var dataReaderMock = fixture.Freeze<Mock<IDataReader>>();

dataReaderMock.Setup(dr => dr.Read())
                      .Returns(true);

然而,这似乎并不符合我的期望。当我调用IDbCommand.ExecuteReader时,得到的读取器与我之前冻结/设置的读取器不同。

以下是一个例子:

var fixture = new Fixture().Customize(new AutoMoqCustomization());

var dataReaderMock = fixture.Freeze<Mock<IDataReader>>();
dataReaderMock.Setup(dr => dr.Read())
              .Returns(true);

//true - Create<IDataReader> retrieves the data reader I just mocked
Assert.AreSame(dataReaderMock.Object, fixture.Create<IDataReader>());

//false - IDbCommand returns a different instance of IDataReader
Assert.AreSame(dataReaderMock.Object, fixture.Create<IDbCommand>().ExecuteReader());

我做错了什么?如何让其他装置,例如IDbCommand,使用模拟的IDataReader实例?

相关链接:https://dev59.com/JHbZa4cB1Zd3GeqPDTk5#18540861 - Mark Seemann
你基本上可以看到这个问题的影响:https://github.com/AutoFixture/AutoFixture/issues/176 - Mark Seemann
1
@MarkSeemann 我明白了...看着MockConfigurator源码,我发现mock的默认值被设置为DefaultValue.Mock,这就是为什么ExecuteReader会得到一个全新的IDataReader mock。我会尝试创建自己的配置器来设置每个方法,使mock回调到fixture并从容器中获取其返回实例。 - dcastro
3个回答

8
从3.20.0版本开始,您可以使用AutoConfiguredMoqCustomization。这将自动配置所有模拟对象,使它们的成员返回值由AutoFixture生成。
例如,IDbConnection.CreateCommand会自动配置为从fixture返回IDbCommandIDbCommand.ExecuteReader会自动配置为从fixture返回一个IDataReader
现在,所有这些测试都应该通过:
var fixture = new Fixture().Customize(new AutoConfiguredMoqCustomization());

var dataReaderMock = fixture.Freeze<Mock<IDataReader>>();
dataReaderMock.Setup(dr => dr.Read())
              .Returns(true);

//all pass
Assert.Same(dataReaderMock.Object, fixture.Create<IDataReader>());
Assert.Same(dataReaderMock.Object, fixture.Create<IDbCommand>().ExecuteReader());
Assert.Same(dataReaderMock.Object, fixture.Create<IDbConnection>().CreateCommand().ExecuteReader());
Assert.Same(dataReaderMock.Object, fixture.Create<Func<IDbConnection>>()().CreateCommand().ExecuteReader());

2
AutoConfiguredMoqCustomization现在已被弃用,请使用AutoMoqCustomization - Spikeh

4

你需要同样冻结 Mock<IDbCommand>,并将其设置为Stub对象以返回现有的dataReaderMock.Object实例。

如果你在测试的Arrange部分添加以下内容,则测试将通过:

var dbCommandStub = 
    fixture
        .Freeze<Mock<IDbCommand>>()
        .Setup(x => x.ExecuteReader())
        .Returns(dataReaderMock.Object);

这是否意味着我还需要冻结IDbConnection并设置其返回dbCommandStub.Object - dcastro
是的,如果您从那里访问DbCommand对象,您也需要类似地“Freeze” IDbConnection - Nikos Baxevanis

1
虽然Nikos的解决方案有效,但我不建议嘲弄ado.net。
在我看来,您的测试可能很难理解、维护,并且不会给您应该拥有的信心。
我建议测试您的数据层,即使速度较慢,也要一直到数据库。
我建议阅读这篇关于最佳mocking实践的文章: http://codebetter.com/jeremymiller/2006/01/10/best-and-worst-practices-for-mock-objects/ 不要嘲笑别人: http://aspiringcraftsman.com/2012/04/01/tdd-best-practices-dont-mock-others/ 我不知道您的具体情况,但我想分享这些内容。

谢谢您的回复。然而,如果我要直接到达数据库,那么我写的就不是单元测试了,而是功能测试。我确实有一套全面的功能测试套件,可以让我获得所需的信心。 - dcastro
1
此外,那篇文章已经写于8年前。当时模拟数据库连接可能很困难(足够困难以至于不值得),但现在情况肯定不是这样了。而且,当我完成为AutoFixture编写插件后,只需要不到2/3行代码就可以实现。 - dcastro
这是我从许多书籍和文章中获得的一般建议。也许其中一些已经有点老了,但我认为它们并没有过时。这里有一篇关于嘲笑他人的较新文章: http://aspiringcraftsman.com/2012/04/01/tdd-best-practices-dont-mock-others/你从测试中获得了什么? - Jakob
我只会为我的基础设施代码编写集成测试和可能的验收测试。但是有些人对此持不同意见。如果你觉得测试并不会增加负担,而且从中获益,那么也许可以忽略我的建议。 - Jakob
1
你上次发布的链接提到了一个关于假设第三方库行为的好观点。因此,我不会将自己与 NLog 或 ActiveMQ 等耦合。相反,我使用我们自己的定制 ILogger 和 ISendAdapter 接口,然后为其提供测试替身。关于数据库连接,由于 IDbConnection 不是特定于供应商的接口,并且是一个众所周知的接口,我认为为其提供测试替身是可以的。虽然我没有集成测试,也许我应该...感谢您的建议。 - dcastro

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接