12

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

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

原始问题:

我昨天开始为我的 xUnit.net 测试开始尝试 AutoFixture,这些测试中都有 Moq。我希望替换一些 Moq 的东西或使它更易于阅读,我对在 SUT Factory 容量中使用 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>());

使测试完全绿色,这条线正是我期望 AutoFixture 在实例化SettingMappingXml.

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

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,Moq 是 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只是一个自动属性。

4

2 回答 2

15

假设该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);

这会打印出这样的内容:

设置Key83b75965-2886-4308-bcc4-eb0f8e63de09

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

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

保护不变量

解决此问题的正确方法是使用单元测试和 AutoFixture 作为反馈机制。这是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方法调用封装在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 的 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));
}

其他选项

要使原始测试通过,您也可以完全关闭 Auto-properties:

fixture.OmitAutoProperties = true;
于 2012-11-22T09:22:22.133 回答
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>),然后注入它,这样当您再次请求它时它总是会返回该实例。

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

在我们请求一个依赖于andStep 3的类型的实例。由于我们使用 Auto Mocking,该类将为这些接口提供模拟。但是,我们之前已经注入了它们,因此现在会自动提供已经创建的模拟。SettingMappingXmlISettingsITracingServiceFixtureFreeze

于 2012-11-21T02:50:43.433 回答