7

给定这个系统来测试:

public class MySut
{
    private readonly IHardToMockDependency _hardToMockDependency;

    public MySut(IHardToMockDependency hardToMockDependency,
                 IOtherDependency otherDependency)
    {
        _hardToMockDependency = hardToMockDependency;
    }

    public string GetResult()
    {
        return _hardToMockDependency.GetResult();
    }
}

public interface IOtherDependency { }

public interface IHardToMockDependency
{
    string GetResult();
}

而这个单元测试:

internal class FakeHardToMockDependency : IHardToMockDependency
{
    private readonly string _result;

    public FakeHardToMockDependency(string result)
    {
        _result = result;
    }

    public string GetResult()
    {
        return _result;
    }
}

public class MyTests
{
    [Fact]
    public void GetResultReturnsExpected()
    {
        string expectedResult = "what I want";
        var otherDependencyDummy = new Mock<IOtherDependency>();
        var sut = new MySut(new FakeHardToMockDependency(expectedResult),
                            otherDependencyDummy.Object);

        var actualResult = sut.GetResult();

        Assert.Equal(expectedResult, actualResult);
    }
}

我应该如何将其转换为使用 AutoFixture.Xunit 和 AutoFixture.AutoMoq(同时仍使用手动伪造)?

在现实世界的测试中,手动伪造的界面和行为会更加复杂。请注意,我想将匿名变量(expectedResult 字符串)传递给手动伪造的构造函数。

4

3 回答 3

10

There are already some good answers here, but I'd like to suggest a simpler alternative that involves slightly loosening the invariants of the FakeHardToMockDependency class. Make it public, and provide a way to assign the result that is decoupled from the constructor:

public class FakeHardToMockDependency : IHardToMockDependency
{
    private string _result;

    public FakeHardToMockDependency(string result)
    {
        _result = result;
    }

    internal string Result
    {
        get { return _result; }
        set { _result = value; }
    }

    public string GetResult()
    {
        return _result;
    }
}

Notice that I've added an internal property and removed the readonly keyword from the field.

This enables you to refactor the original test to this:

[Theory, AutoMoqData]
public void GetResultReturnsExpected_AutoDataVersion(
    [Frozen(As = typeof(IHardToMockDependency))]FakeHardToMockDependency fake,
    MySut sut)
{
    var expected = "what I want";
    fake.Result = expected;

    var actual = sut.GetResult();

    Assert.Equal(expected, actual);
}

For completeness, here's the AutoMoqDataAttribute code:

public class AutoMoqDataAttribute : AutoDataAttribute
{
    public AutoMoqDataAttribute()
        : base(new Fixture().Customize(new AutoMoqCustomization()))
    {
    }
}
于 2013-03-16T08:18:40.290 回答
4

Depending on what kind of parameters you need to pass to your manual fake, you might be able to use a parameterized attribute, similar to the AutoFixture's built-in InlineAutoDataAttribute.

Given these

public interface IHardToMockDependency
{
    string Value { get; }
}

public class FakeHardToMockDependency : IHardToMockDependency
{
    private readonly string _value;

    public FakeHardToMockDependency(string value)
    {
        _value = value;
    }

    #region IHardToMockDependency Members

    public string Value
    {
        get { return this._value; }
    }

    #endregion IHardToMockDependency Members
}

you create an ICustomization implementation that tells a fixture object how to create an implemention of the IHardToFakeDependency interface:

public class FakeHardToMockDependencyCustomization : ICustomization
{
    private readonly string _value;

    public FakeHardToMockDependencyCustomization(string value)
    {
        _value = value;
    }

    #region ICustomization Members

    public void Customize(IFixture fixture)
    {
        fixture.Register<IHardToMockDependency>(() => new FakeHardToMockDependency(this._value));
    }

    #endregion ICustomization Members
}

Note that this needs to know the string you want to pass in, of course.

Next, you roll up this with the other customizations you want to use in a CompositeCustomization:

public class ManualFakeTestConventions : CompositeCustomization
{
    public ManualFakeTestConventions(string value)
        : base(new FakeHardToMockDependencyCustomization(value), new AutoMoqCustomization())
    {
    }
}

Make sure you always put the customizations in order from most specific to most general, as explained here by Mark Seemann.

Now you create an AutoDataAttribute implementation that uses this customization:

public class ManualFakeAutoDataAttribute : AutoDataAttribute
{
    public ManualFakeAutoDataAttribute(string value)
        : base(new Fixture().Customize(new ManualFakeTestConventions(value)))
    {
    }
}

This can now be used in the same way as an InlineAutoDataAttribute:

public class ManualFakeTests
{
    [Theory, ManualFakeAutoData("iksdee")]
    public void ManualFake(IHardToMockDependency fake)
    {
        Assert.IsType<FakeHardToMockDependency>(fake);
        Assert.Equal("iksdee", fake.Value);
    }
}

You can also have it injected into an auto-created SUT instance right away by applying the [Frozen] attribute to the Theory parameter:

    [Theory, ManualFakeAutoData("iksdee")]
    public void SutWithManualFake([Frozen] IHardToMockDependency fake, MySut sut)
    {

    }

This will create a MySut instance and the IHardToMockDependency instance needed for the constructor, for which you have given AutoFixture a rule in the FakeHardToMockDependencyCustomization, and also give you that very instance as the fake variable.

Note that not freezing the fake would still give you a correct FakeHardToMockDependency instance as well as inject one into the sut, but those would be distinct, as we have registered a factory delegate in the customization. Freezing the instance will cause the fixture to always return the same instance for subsequent requests for the interface.

This has a few caveats, however:

  • You have no reference to the string you pass in as a parameter, so you have to have it in there as a string literal twice. You can work around this with string constants within the test class, for example.
  • The number of types that can be used as attribute parameters in .NET is limited. As long as you only need basic types, you should be fine, but calling constructors or the like in the parameter list is not possible.
  • You should only use this attribute when you need an instance if IHardToFakeDependency; otherwise you'll always have to pass in a string parameter anyway. If you have a set of standard customizations you need to use, create another attribute that contains only those.
  • If you need the capabilities of the InlineAutoDataAttribute at the same time, you also need to create yet another attribute that combines the features of both.

Depending on the concrete circumstances, you might also want to look at xUnit.net's PropertyDataAttribute, but I hardly ever find myself using that.

In general, in my opinion, understanding how to work with customizations and autodata attributes and when and how to create your own are the key to effectively using AutoFixture and really make it save you work.

If you often write code in a specific domain that you need to test, it might well make sense to create a library containing customizations, attribute and stub objects that will always be ready to use once you drop it in next to xUnit.net, AutoFixture and Moq. I know I'm damn glad I built mine.

Oh, and also: Having a dependency that is hard to mock might point at a design issue. Why is it that hard to mock?

于 2013-03-16T01:43:05.823 回答
3

也许这不是最惯用的 Autofixture 设置,但绝对有效:

[Fact]
public void GetResultReturnsExpected()
{
    var fixture = new Fixture()
        .Customize(new AutoMoqCustomization());

    var expectedResult = fixture.Create<string>();

    fixture.Register<IHardToMockDependency>(
        () => new FakeHardToMockDependency(expectedResult));

    var sut = fixture.Create<MySut>();

    var actualResult = sut.GetResult();

    Assert.Equal(expectedResult, actualResult);
}

如果您也想使用,您可以根据这篇很棒的文章AutoData创建自己的,您可以在其中隐藏部分或全部夹具自定义。AutoMoqData

就像是:

public class MySutAutoDataAttribute : AutoDataAttribute
{
    public MySutAutoData()
        : base(new Fixture()
            .Customize(new AutoMoqCustomization()))
    {
        Fixture.Freeze<string>();

        Fixture.Register<IHardToMockDependency>(
            () => new FakeHardToMockDependency(Fixture.Create<string>()));
    }
}

你可以像这样使用它:

[Theory, MySutAutoData]
public void GetResultReturnsExpected(MySut sut, string expectedResult)
{
    var actualResult = sut.GetResult();

    Assert.Equal(expectedResult, actualResult);
}

But you should note that there is room for a lots of improvement in the MySutAutoDataAttribute for example: it is not very generic and Fixture.Freeze<string>(); can cause problems if you are using multiple strings in your tests.

于 2013-03-15T22:31:19.130 回答