,现在已经找到了解决方案的短期外卖:
AutoFixture返回冻结的模拟很好;我的sut也是由AutoFixture生成的,它有一个具有本地默认值的公共属性,这对测试很重要,并且AutoFixture设置为一个新的值。从马克的回答中可以学到更多的东西。
原始问题:
昨天,我开始尝试AutoFixture,因为我的xUnit.net测试中到处都是Moq。我希望能替换一些Moq的内容,或者让它更容易阅读,我特别感兴趣的是在SUT工厂的容量中使用AutoFixture。
我为自己准备了一些Mark在AutoMocking上的博客文章,并试图从那里开始工作,但我没有走多远。
如果没有AutoFixture,我的测试就是这样的:
[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对象,然后手动注入依赖项(不是因为这个测试太复杂,而是因为它足够简单,可以尝试并理解它们)。
输入AutoFixture -第一次尝试:
[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>());使测试完全变成绿色,这一行正是我期望AutoFixture在内部实例化SettingMappingXml时所做的。
然后是冻结组件的概念,所以我直接冻结了夹具中的模拟,而不是获取模拟对象:
fixture.Freeze<Mock<ISettings>>(f => f.Do(m => m.Setup(s => s.Get(settingKey)).Returns(xmlString)));当然,这非常好--只要我显式地调用SettingMappingXml构造函数,并且不依赖于CreateAnonymous()。
简单地说,我不明白它为什么像表面上那样工作,因为它违背了我能想到的任何逻辑。通常情况下,我会怀疑库中有一个bug,但是这是非常基本的东西,我相信其他人也会遇到这个错误,而且它早就被发现并修复了。更重要的是,了解Mark对测试和DI的勤奋方法,这不可能是无意的。
这反过来意味着我一定是错过了一些相当基础的东西。我如何使用预先配置的模拟对象作为依赖项由AutoFixture创建SUT?我现在唯一确定的是,我需要AutoMoqCustomization,所以我不需要为ITracingService配置任何东西。
AutoFixture/AutoMoq包为2.14.1,Moq为3.1.416.3,全部来自NuGet。.NET版本为4.5 (与VS2012一起安装),VS2012和2010年的行为相同。
在撰写这篇文章时,我发现有些人在Moq4.0和程序集绑定重定向方面有问题,所以我仔细地清除了我的解决方案中的任何Moq 4实例,并通过将AutoFixture.AutoMoq安装到“干净”项目中安装了Moq3.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只是一个自动属性。
发布于 2012-11-22 09:22:22
假设SettingKey属性定义如下,我现在可以重现这个问题:
public string SettingKey { get; set; }所发生的是注入到SettingMappingXml实例中的SettingMappingXml非常好,但是由于SettingKey是可写的,因此AutoFixture的AutoFixture特性启动并修改了该值。
请考虑以下代码:
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var sut = fixture.CreateAnonymous<SettingMappingXml>();
Console.WriteLine(sut.SettingKey);这个打印的东西是这样的:
SettingKey83b75965-2886-4308-bcc4-eb0f8e63de09
即使所有的测试双变量都被正确地注入,Setup方法中的期望也没有得到满足。
解决这一问题的方法有很多。
保护不变量
解决这个问题的正确方法是使用单元测试和AutoFixture作为反馈机制。这是GOOS的关键之一:单元测试的问题通常是设计缺陷的症状,而不是单元测试(或AutoFixture)本身的错误。
在这种情况下,它向我表示the design isn't fool-proof enough。客户端可以随意操作SettingKey真的合适吗?
作为最低限度,我建议采用这样的替代实现:
public string SettingKey { get; private set; }有了那个变化,我的复制品就通过了。
省略SettingKey
如果不能(或不会)更改设计,可以指示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方法调用封装在a Customization中。
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's xUnit.net integration,但是在这样做之前,您需要使用一种不那么强制的方式来操作Fixture。事实证明,创建、配置和注入ISettings测试双的代码是非常惯用的,因此它有一个名为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;发布于 2012-11-21 02:50:43
在第一个测试中,您可以创建一个Fixture类的实例,并应用AutoMoqCustomization:
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 1和Step 2中所做的。
在Step 3中,我们请求一个依赖于ISettings和ITracingService的SettingMappingXml类型的实例。由于我们使用自动模拟,所以Fixture类将为这些接口提供模拟。但是,我们以前已经向它们注入了Freeze,所以已经创建的模拟现在自动提供了。
https://stackoverflow.com/questions/13484471
复制相似问题