【问题标题】:Autofixture generate custom listAutofixture 生成自定义列表
【发布时间】:2013-07-24 22:51:58
【问题描述】:

这是我的对象:

public class Symbol
{
   private readonly string _identifier;
   private readonly IList<Quote> _historicalQuotes;

   public Symbol(string identifier, IEnumerable<Quote> historicalQuotes = null)
   {
      _identifier = identifier;
      _historicalQuotes = historicalQuotes;
   }
}

public class Quote
{
    private readonly DateTime _tradingDate;
    private readonly decimal _open;
    private readonly decimal _high;
    private readonly decimal _low;
    private readonly decimal _close;
    private readonly decimal _closeAdjusted;
    private readonly long _volume;

    public Quote(
        DateTime tradingDate,
        decimal open,
        decimal high,
        decimal low,
        decimal close,
        decimal closeAdjusted,
        long volume)
    {
        _tradingDate = tradingDate;
        _open = open;
        _high = high;
        _low = low;
        _close = close;
        _closeAdjusted = closeAdjusted;
        _volume = volume;
    }
}

我需要一个填充了引用列表的 Symbol 实例。

在我的测试中,我想验证我是否可以返回收盘价低于或高于特定值的所有报价。这是我的测试:

[Fact]
public void PriceUnder50()
{
    var msftIdentifier = "MSFT";
    var quotes = new List<Quote>
    {
        new Quote(DateTime.Parse("01-01-2009"), 0, 0, 0, 49, 0, 0), 
        new Quote(DateTime.Parse("01-02-2009"), 0, 0, 0, 51, 0, 0), 
        new Quote(DateTime.Parse("01-03-2009"), 0, 0, 0, 50, 0, 0), 
        new Quote(DateTime.Parse("01-04-2009"), 0, 0, 0, 10, 0, 0)
    };

    _symbol = new Symbol(msftIdentifier, quotes);


    var indicator = new UnderPriceIndicator(50);
    var actual = indicator.Apply(_symbol);

    Assert.Equal(2, actual.Count);
    Assert.True(actual.Any(a => a.Date == DateTime.Parse("01-01-2009")));
    Assert.True(actual.Any(a => a.Date == DateTime.Parse("01-04-2009")));
    Assert.True(actual.Any(a => a.Price == 49));
    Assert.True(actual.Any(a => a.Price == 10));
}

好的。

现在,我想使用 Autofixture 来完成,我是一个真正的初学者。我已经在互联网上阅读了几乎所有关于此工具的内容(作者的博客、codeplex 常见问题解答、github 源代码)。我了解 autofixture 的基本功能,但现在我想在我的实际项目中使用 autofixture。这是我迄今为止尝试过的。

var msftIdentifier = "MSFT";

var quotes = new List<Quote>();
var random = new Random();
fixture.AddManyTo(
    quotes,
    () => fixture.Build<Quote>().With(a => a.Close, random.Next(1,49)).Create());
quotes.Add(fixture.Build<Quote>().With(a => a.Close, 49).Create());

_symbol = new Symbol(msftIdentifier, quotes);

// I would just assert than 49 is in the list
Assert.True(_symbol.HistoricalQuotes.Contains(new Quote... blabla 49));

理想情况下,我更愿意直接创建 Symbol 的夹具,但我不知道如何自定义我的报价列表。而且我不确定我的测试是否通用,因为在另一个测试中,我需要检查特定值是否高于而不是低于,所以我将复制“夹具代码”并手动添加引号 = 51.

所以我的问题是:

1 - 这是我应该如何使用 autofixture 的方式吗?

2 - 是否可以改进我在示例中使用 autofixture 的方式?

【问题讨论】:

    标签: refactoring autofixture


    【解决方案1】:

    AutoFixture 最初是作为测试驱动开发 (TDD) 的工具而构建的,而 TDD 完全是关于反馈。本着GOOS 的精神,您应该聆听您的测试。如果测试很难编写,您应该考虑您的 API 设计。 AutoFixture 倾向于放大这种反馈,这就是它告诉我的。

    比较容易

    首先,虽然与 AutoFixture 无关,但 Quote 类只是 请求 转换为适当的 值对象,因此我将覆盖 Equals 以更容易比较预期和实际实例:

    public override bool Equals(object obj)
    {
        var other = obj as Quote;
        if (other == null)
            return base.Equals(obj);
    
        return _tradingDate == other._tradingDate
            && _open == other._open
            && _high == other._high
            && _low == other._low
            && _close == other._close
            && _closeAdjusted == other._closeAdjusted
            && _volume == other._volume;
    }
    

    (确保也覆盖GetHashCode。)

    复制和更新

    上面的测试尝试似乎暗示我们缺乏一种方法来改变单个字段同时保持其余字段不变。从函数式语言中汲取灵感,我们可以在 Quote 类本身上引入一种方法:

    public Quote WithClose(decimal newClose)
    {
        return new Quote(
            _tradingDate,
            _open,
            _high,
            _low,
            newClose,
            _closeAdjusted,
            _volume);
    }
    

    这种 API 在值对象上往往非常有用,以至于我总是将此类方法添加到我的值对象中。

    让我们对Symbol做同样的事情:

    public Symbol WithHistoricalQuotes(IEnumerable<Quote> newHistoricalQuotes)
    {
        return new Symbol(_identifier, newHistoricalQuotes);
    }
    

    这使得让 AutoFixture 处理所有你不关心的事情变得更容易,同时明确说明你关心的事情。

    使用 AutoFixture 进行测试

    原来的测试现在可以改写为:

    [Fact]
    public void PriceUnder50()
    {
        var fixture = new Fixture();
        var quotes = new[]
        {
            fixture.Create<Quote>().WithClose(49),
            fixture.Create<Quote>().WithClose(51),
            fixture.Create<Quote>().WithClose(50),
            fixture.Create<Quote>().WithClose(10),
        };
        var symbol = fixture.Create<Symbol>().WithHistoricalQuotes(quotes);
        var indicator = fixture.Create<UnderPriceIndicator>().WithLimit(50);
    
        var actual = indicator.Apply(symbol);
    
        var expected = new[] { quotes[0], quotes[3] };
        Assert.Equal(expected, actual);
    }
    

    此测试仅说明您关心的测试用例的那些部分,而 AutoFixture 会处理对测试用例没有任何影响的所有其他值。这使得测试更加健壮,并且更具可读性。

    【讨论】:

    • +1 @Gui 和其他人 Mark 太客气了,无法举报它,但他的 PluralSight Advanced Unit Testing 课程充满了这种善良。值得花钱,但更好的是他们有免费试用,而且我想不出更好的课程来使用它(当然Outside In Test Driven Development 紧随其后)
    • @Mark Seemann 感谢您的评论马克。我同意你的测试肯定更具可读性。我本来打算再问一些问题,但你的答案太完整了,每次我重新阅读你的答案时,它都会回答我的每一个问题。天哪,你可能是单元测试之神。
    • WithCloseWithLimit在测试项目中定义添加扩展方法,而不是作为实例方法添加怎么样? (现在在 C#6 中,它们甚至不需要在静态类中)
    • @BjörnAliGöransson 你可以,但为什么你要故意让一些明显有利于设计的“生产代码”远离?
    • 注意。写完这篇文章后不久,尽管我最初对 YAGNI 感到担忧,但我发现自己正在使用@MarkSeemann。
    猜你喜欢
    • 2021-09-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多