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

时间:2012-11-21 00:21:41

标签: c# unit-testing autofixture automocking

现在找到解决方案的简短内容:

AutoFixture返回冻结模拟就好了;我的sut也是由AutoFixture生成的,只有一个公共属性,其本地默认值对于测试非常重要,并且AutoFixture设置为新值。除了Mark的答案之外,还有很多值得学习的东西。

原始问题:

我昨天开始尝试使用AutoFixture进行我的xUnit.net测试,这些测试都有Moq。我希望能够更换一些Moq或者更容易阅读,我对在SUT工厂容量中使用AutoFixture特别感兴趣。

我在Mark Seemann的一些关于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>());

使测试完全变为绿色,这一行正是我在实例化SettingMappingXml时所期望的AutoFixture内部做的。

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

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

果然这完全正常 - 只要我明确地调用SettingMappingXml构造函数并且不依赖CreateAnonymous()



简单地说,我不明白它为什么会以它显然的方式工作,因为它违背了我能想到的任何逻辑。 通常情况下,我会怀疑库中有一个错误,但这是一个非常基本的东西,我相信其他人会遇到这个问题,而且很久以来就会发现并修复它。更重要的是,了解马克对测试和DI的刻苦态度,这不可能是无意的。

这反过来意味着我必须错过一些相当基本的东西。如何将AutoFixture创建的SUT与预先配置的模拟对象作为依赖项?我现在唯一确定的是我需要AutoMoqCustomization所以我不需要为ITracingService配置任何内容。

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

在撰写这篇文章时,我发现有些人在使用Moq 4.0和程序集绑定重定向时出现问题,所以我通过将AutoFixture.AutoMoq安装到“clean”中,精心清除了Moq 4的任何实例的解决方案并安装了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只是一个自动属性。

2 个答案:

答案 0 :(得分:13)

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

public string SettingKey { get; set; }

注入SettingMappingXml实例的Test Doubles完全正常,但由于SettingKey是可写的,因此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 Test 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;

答案 1 :(得分:4)

在第一个测试中,您可以使用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>),然后注入,以便在您再次请求时始终返回该实例。< / p>

  

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

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