AutoFixture/AutoMoq 忽略注入的实例/冻结模拟



现在找到解决方案的简短收获:

AutoFixture 返回冻结模拟就好了;我的 sut 也是由 AutoFixture 生成的,它只有一个公共属性,该属性具有本地默认值,该属性对测试很重要,并且 AutoFixture 设置为新值。除此之外,从马克的回答中还有很多东西需要学习。

原始问题:

我昨天开始尝试AutoFixture进行我的 xUnit.net 测试,这些测试都有最小起订量。我希望替换一些Moq的东西或使其更易于阅读,我对在SUT工厂中使用AutoFixture特别感兴趣。

我用马克·西曼(Mark Seemann)关于AutoMocking的一些博客文章武装自己,并试图从那里开始工作,但我并没有走得太远。

这是我的测试在没有自动固定的情况下的样子:

[Fact]
public void GetXml_ReturnsCorrectXElement()
{
// Arrange
string xmlString = @"
<mappings>
<mapping source='gcnm_loan_amount_min' target='gcnm_loan_amount_min_usd' />
<mapping source='gcnm_loan_amount_max' target='gcnm_loan_amount_max_usd' />
</mappings>";
string settingKey = "gcCreditApplicationUsdFieldMappings";
Mock<ISettings> settingsMock = new Mock<ISettings>();
settingsMock.Setup(s => s.Get(settingKey)).Returns(xmlString);
ISettings settings = settingsMock.Object;
ITracingService tracing = new Mock<ITracingService>().Object;
XElement expectedXml = XElement.Parse(xmlString);
IMappingXml sut = new SettingMappingXml(settings, tracing);
// Act
XElement actualXml = sut.GetXml();
// Assert
Assert.True(XNode.DeepEquals(expectedXml, actualXml));
}

这里的故事很简单 - 确保SettingMappingXml使用正确的键(硬编码/属性注入)查询ISettings依赖项,并将结果作为XElement返回。仅当出现错误时,ITracingService才相关。

我试图做的是摆脱显式创建ITracingService对象然后手动注入依赖项的需要(不是因为这个测试太复杂,而是因为它足够简单,可以尝试并理解它们)。

进入自动固定 - 第一次尝试:

[Fact]
public void GetXml_ReturnsCorrectXElement()
{
// Arrange
IFixture fixture = new Fixture();
fixture.Customize(new AutoMoqCustomization());
string xmlString = @"
<mappings>
<mapping source='gcnm_loan_amount_min' target='gcnm_loan_amount_min_usd' />
<mapping source='gcnm_loan_amount_max' target='gcnm_loan_amount_max_usd' />
</mappings>";
string settingKey = "gcCreditApplicationUsdFieldMappings";
Mock<ISettings> settingsMock = new Mock<ISettings>();
settingsMock.Setup(s => s.Get(settingKey)).Returns(xmlString);
ISettings settings = settingsMock.Object;
fixture.Inject(settings);
XElement expectedXml = XElement.Parse(xmlString);
IMappingXml sut = fixture.CreateAnonymous<SettingMappingXml>();
// Act
XElement actualXml = sut.GetXml();
// Assert
Assert.True(XNode.DeepEquals(expectedXml, actualXml));
}

我希望CreateAnonymous<SettingMappingXml>()在检测到ISettings构造函数参数时,注意到已为该接口注册了一个具体实例并注入该实例 - 但是,它不会这样做,而是创建一个新的匿名实现。

这尤其令人困惑,因为fixture.CreateAnonymous<ISettings>()确实返回了我的实例 -

IMappingXml sut = new SettingMappingXml(fixture.CreateAnonymous<ISettings>(), fixture.CreateAnonymous<ITracingService>());

使测试完全绿色,而这一行正是我在实例化SettingMappingXml时期望AutoFixture在内部执行的操作。

然后是冻结组件的概念,所以我继续在灯具中冻结模拟,而不是获取模拟对象:

fixture.Freeze<Mock<ISettings>>(f => f.Do(m => m.Setup(s => s.Get(settingKey)).Returns(xmlString)));

果然,这工作得很好 - 只要我显式调用SettingMappingXml构造函数并且不依赖于CreateAnonymous().



简单地说,我不明白为什么它以这种方式工作,因为它违背了我能想到的任何逻辑。 通常我会怀疑库中存在错误,但这是如此基本的东西,我相信其他人会遇到这个,并且早就被发现并修复了它。更重要的是,知道 Mark 对测试和 DI 的刻苦方法,这不可能是无意的。

这反过来意味着我一定错过了一些相当基本的东西。如何让 AutoFixture 使用预配置的模拟对象作为依赖项创建我的 SUT?我现在唯一确定的是我需要AutoMoqCustomization,这样我就不必为ITracingService配置任何东西。

AutoFixture/AutoMoq 包是 2.14.1,最小起订量是 3.1.416.3,全部来自 NuGet。 .NET 版本为 4.5(随 VS2012 一起安装),VS2012 和 2010 中的行为相同。

在写这篇文章时,我发现有些人在 Moq 4.0 和程序集绑定重定向方面遇到了问题,所以我一丝不苟地清除了我的解决方案中的任何 Moq 4 实例,并通过安装 AutoFixture.AutoMoq 安装到 "干净" 项目中来安装 Moq 3.1。但是,我的测试行为保持不变。

感谢您的任何指示和解释。

更新:这是 Mark 要求的构造函数代码:

public SettingMappingXml(ISettings settingSource, ITracingService tracing)
{
this._settingSource = settingSource;
this._tracing = tracing;
this.SettingKey = "gcCreditApplicationUsdFieldMappings";
}

为了完整起见,GetXml()方法如下所示:

public XElement GetXml()
{
int errorCode = 10600;
try
{
string mappingSetting = this._settingSource.Get(this.SettingKey);
errorCode++;
XElement mappingXml = XElement.Parse(mappingSetting);
errorCode++;
return mappingXml;
}
catch (Exception e)
{
this._tracing.Trace(errorCode, e.Message);
throw;
}
}

SettingKey只是一个自动属性。

假设SettingKey属性定义如下,我现在可以重现该问题:

public string SettingKey { get; set; }

发生的情况是,注入到 SettingMappingXml 实例中的测试替身完全没问题,但由于SettingKey是可写的,AutoFixture 的自动属性功能会启动并修改值。

请考虑以下代码:

var fixture = new Fixture().Customize(new AutoMoqCustomization());
var sut = fixture.CreateAnonymous<SettingMappingXml>();
Console.WriteLine(sut.SettingKey);

这将打印如下内容:

设置键83b75965-2886-4308-bcc4-eb0f8e63de09

即使正确注入了所有测试替身,也无法满足Setup方法中的预期。

有很多方法可以解决此问题。

保护不变量

解决此问题的正确方法是使用单元测试和自动固定装置作为反馈机制。这是GOOS中的关键点之一:单元测试的问题通常是设计缺陷的症状,而不是单元测试(或AutoFixture)本身的错误。

在这种情况下,它向我表明该设计不够万无一失。客户端可以随意操纵SettingKey真的合适吗?

至少,我会推荐这样的替代实现:

public string SettingKey { get; private set; }

随着这种更改,我的重现通过了。

省略设置键

如果不能(或不会)更改设计,可以指示 AutoFixture 跳过设置SettingKey属性:

IMappingXml sut = fixture
.Build<SettingMappingXml>()
.Without(s => s.SettingKey)
.CreateAnonymous();

就个人而言,我发现每次需要特定类的实例时都必须编写一个Build表达式会适得其反。您可以将SettingMappingXml实例的创建方式与实际实例化分离:

fixture.Customize<SettingMappingXml>(
c => c.Without(s => s.SettingKey));
IMappingXml sut = fixture.CreateAnonymous<SettingMappingXml>();

若要更进一步,可以将该Customize方法调用封装在自定义项中。

public class SettingMappingXmlCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customize<SettingMappingXml>(
c => c.Without(s => s.SettingKey));
}
}

这要求您使用该自定义创建Fixture实例:

IFixture fixture = new Fixture()
.Customize(new SettingMappingXmlCustomization())
.Customize(new AutoMoqCustomization());

一旦您获得两个或三个以上的自定义项链接,您可能会厌倦一直编写该方法链。是时候将这些自定义项封装到特定库的一组约定中了:

public class TestConventions : CompositeCustomization
{
public TestConventions()
: base(
new SettingMappingXmlCustomization(),
new AutoMoqCustomization())
{
}
}

这使您能够始终创建如下所示的Fixture实例:

IFixture fixture = new Fixture().Customize(new TestConventions());

TestConventions为您提供了一个中心位置,您可以在需要时偶尔修改测试套件的约定。它减少了单元测试的可维护性负担,并有助于保持生产代码的设计更加一致。

最后,由于看起来好像您正在使用 xUnit.net,因此您可以利用AutoFixture的 xUnit.net 集成,但是在此之前,您需要使用不太命令性的方法来操作Fixture。事实证明,创建、配置和注入ISettingsTest Double 的代码非常惯用,以至于它有一个名为 Freeze 的快捷方式:

fixture.Freeze<Mock<ISettings>>()
.Setup(s => s.Get(settingKey)).Returns(xmlString);

完成此操作后,下一步是定义自定义 AutoDataAttribute:

public class AutoConventionDataAttribute : AutoDataAttribute
{
public AutoConventionDataAttribute()
: base(new Fixture().Customize(new TestConventions()))
{
}
}

您现在可以将测试简化到最基本的内容,摆脱所有噪音,使测试能够简洁地仅表达重要内容:

[Theory, AutoConventionData]
public void ReducedTheory(
[Frozen]Mock<ISettings> settingsStub,
SettingMappingXml sut)
{
string xmlString = @"
<mappings>
<mapping source='gcnm_loan_amount_min' target='gcnm_loan_amount_min_usd' />
<mapping source='gcnm_loan_amount_max' target='gcnm_loan_amount_max_usd' />
</mappings>";
string settingKey = "gcCreditApplicationUsdFieldMappings";
settingsStub.Setup(s => s.Get(settingKey)).Returns(xmlString);
XElement actualXml = sut.GetXml();
XElement expectedXml = XElement.Parse(xmlString);
Assert.True(XNode.DeepEquals(expectedXml, actualXml));
}

其他选项

要使原始测试通过,您也可以完全关闭自动属性:

fixture.OmitAutoProperties = true;

在第一个测试中,您可以创建应用AutoMoqCustomizationFixture类的实例:

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

然后,唯一的更改是:

步骤 1

// The following line:
Mock<ISettings> settingsMock = new Mock<ISettings>();
// Becomes:
Mock<ISettings> settingsMock = fixture.Freeze<Mock<ISettings>>();

步骤 2

// The following line:
ITracingService tracing = new Mock<ITracingService>().Object;
// Becomes:
ITracingService tracing = fixture.Freeze<Mock<ITracingService>>().Object;

步骤 3

// The following line:
IMappingXml sut = new SettingMappingXml(settings, tracing);
// Becomes:
IMappingXml sut = fixture.CreateAnonymous<SettingMappingXml>();

就是这样!


以下是它的工作原理:

在内部,Freeze创建所请求类型的实例(例如Mock<ITracingService>),然后注入它,以便在您再次请求它时它将始终返回该实例。

这就是我们在Step 1Step 2所做的。

Step 3中,我们请求一个SettingMappingXml类型的实例,该实例取决于ISettingsITracingService。由于我们使用自动模拟,Fixture类将为这些接口提供模拟。但是,我们之前已经为它们注入Freeze因此现在会自动提供已经创建的模拟。

最新更新