【问题标题】:How to implement date restrictions with AutoFixture?如何使用 AutoFixture 实施日期限制?
【发布时间】:2015-02-19 10:21:47
【问题描述】:

我目前有一个包含多个属性的模型类。简化的模型可能如下所示:

public class SomeClass
{
    public DateTime ValidFrom { get; set; }
    public DateTime ExpirationDate { get; set; }
}

现在我正在使用 NUnit 实现一些单元测试,并使用 AutoFixture 创建一些随机数据:

[Test]
public void SomeTest()
{ 
    var fixture = new Fixture();
    var someRandom = fixture.Create<SomeClass>();
}

到目前为止,这很完美。但是要求ValidFrom的日期总是在ExpirationDate之前。我必须确保这一点,因为我正在实施一些积极的测试。

那么有没有一种简单的方法可以通过使用 AutoFixture 来实现这一点?我知道我可以创建一个修复日期并添加一个随机的日期间隔来解决这个问题,但如果 AutoFixture 可以自己处理这个要求,那就太好了。

我对 AutoFixture 没有太多经验,但我知道我可以通过调用 Build 方法获得 ICustomizationComposer

var fixture = new Fixture();
var someRandom = fixture.Build<SomeClass>()
    .With(some => /*some magic like some.ValidFrom < some.ExpirationDate here...*/ )
    .Create();

也许这是实现这一目标的正确方法?

提前感谢您的帮助。

【问题讨论】:

    标签: c# autofixture


    【解决方案1】:

    可能很想问如何使 AutoFixture 适应我的设计?,但通常,更有趣的问题可能是:我如何使我的设计更健壮吗?

    您可以保留设计并“修复”AutoFixture,但我认为这不是一个特别好的主意。

    在我告诉你如何做到这一点之前,根据你的要求,也许你需要做的就是以下。

    显式赋值

    为什么不简单地为ExpirationDate 分配一个有效值,就像这样?

    var sc = fixture.Create<SomeClass>();
    sc.ExpirationDate = sc.ValidFrom + fixture.Create<TimeSpan>();
    
    // Perform test here...
    

    如果你使用AutoFixture.Xunit,它可以更简单:

    [Theory, AutoData]
    public void ExplicitPostCreationFix_xunit(
        SomeClass sc,
        TimeSpan duration)
    {
        sc.ExpirationDate = sc.ValidFrom + duration;
    
        // Perform test here...
    }
    

    这是相当稳健的,因为即使 AutoFixture (IIRC) 会创建随机的 TimeSpan 值,它们也会保持在正值范围内,除非您对 fixture 进行了更改以改变其行为。

    如果您需要测试SomeClass 本身,这种方法将是解决您的问题的最简单方法。另一方面,如果您需要SomeClass 作为无数其他测试的输入值,则不太实用。

    在这种情况下,修复 AutoFixture 是很有诱惑力的,这也是可能的:

    改变 AutoFixture 的行为

    现在您已经了解了如何将问题作为一次性解决方案来解决,您可以将其作为SomeClass 生成方式的一般变化告诉 AutoFixture:

    fixture.Customize<SomeClass>(c => c
        .Without(x => x.ValidFrom)
        .Without(x => x.ExpirationDate)
        .Do(x => 
            {
                x.ValidFrom = fixture.Create<DateTime>();
                x.ExpirationDate = 
                    x.ValidFrom + fixture.Create<TimeSpan>();
            }));
    // All sorts of other things can happen in between, and the
    // statements above and below can happen in separate classes, as 
    // long as the fixture instance is the same...
    var sc = fixture.Create<SomeClass>();
    

    您还可以将上述对Customize 的调用打包到ICustomization 实现中,以供进一步重用。这也将使您能够将自定义的 Fixture 实例与 AutoFixture.Xunit 一起使用。

    更改 SUT 的设计

    虽然上述解决方案描述了如何更改 AutoFixture 的行为,但 AutoFixture 最初是作为 TDD 工具编写的,而 TDD 的主要目的是提供有关被测系统 (SUT) 的反馈。 AutoFixture 倾向于放大这种反馈,这里也是如此。

    考虑SomeClass的设计。没有什么能阻止客户做这样的事情:

    var sc = new SomeClass
    {
        ValidFrom = new DateTime(2015, 2, 20),
        ExpirationDate = new DateTime(1900, 1, 1)
    };
    

    这编译和运行没有错误,但可能不是你想要的。因此,AutoFixture 实际上并没有做错什么。 SomeClass 没有正确保护其不变量。

    这是一个常见的设计错误,开发人员往往过于信任成员姓名的语义信息。他们的想法似乎是没有人会在他们的正常头脑中将 ExpirationDate 设置为 before ValidFrom 的值!这种论点的问题在于,它假定所有开发人员将始终成对地分配这些值。

    但是,客户端也可能会收到传递给他们的 SomeClass 实例,并希望更新其中一个值,例如:

    sc.ExpirationDate = new DateTime(2015, 1, 31);
    

    这有效吗?你怎么知道?

    客户可以看sc.ValidFrom,但为什么要看呢? 封装的整个目的就是减轻客户的负担。

    相反,您应该考虑更改设计SomeClass。我能想到的最小的设计变化是这样的:

    public class SomeClass
    {
        public DateTime ValidFrom { get; set; }
        public TimeSpan Duration { get; set; }
        public DateTime ExpirationDate
        {
            get { return this.ValidFrom + this.Duration; }
        }
    }
    

    这会将ExpirationDate 变成一个只读的计算属性。有了这个改变,AutoFixture 就可以开箱即用了:

    var sc = fixture.Create<SomeClass>();
    
    // Perform test here...
    

    您也可以将它与 AutoFixture.Xunit 一起使用:

    [Theory, AutoData]
    public void ItJustWorksWithAutoFixture_xunit(SomeClass sc)
    {
        // Perform test here...
    }
    

    这仍然有点脆弱,因为虽然默认情况下 AutoFixture 会创建正的 TimeSpan 值,但也可以更改该行为。

    此外,该设计实际上允许客户端将负的TimeSpan 值分配给Duration 属性:

    sc.Duration = TimeSpan.FromHours(-1);
    

    这是否应该被允许取决于领域模型。一旦你开始考虑这种可能性,实际上可能会发现定义时间倒退的时间段在域中是有效的......

    根据 Postel 定律设计

    如果问题域不允许回溯,您可以考虑在Duration 属性中添加保护子句,拒绝负时间跨度。

    但是,就我个人而言,当我认真对待Postel's Law 时,我经常会发现我得到了更好的 API 设计。在这种情况下,为什么不改变设计让SomeClass 始终使用absolute TimeSpan 而不是签名的TimeSpan

    在这种情况下,我更喜欢不可变对象,它不会强制执行两个 DateTime 实例的角色,直到它知道它们的值:

    public class SomeClass
    {
        private readonly DateTime validFrom;
        private readonly DateTime expirationDate;
    
        public SomeClass(DateTime x, DateTime y)
        {
            if (x < y)
            {
                this.validFrom = x;
                this.expirationDate = y;
            }
            else
            {
                this.validFrom = y;
                this.expirationDate = x;
            }
        }
    
        public DateTime ValidFrom
        {
            get { return this.validFrom; }
        }
    
        public DateTime ExpirationDate
        {
            get { return this.expirationDate; }
        }
    }
    

    与之前的重新设计一样,这个可以使用 AutoFixture 开箱即用:

    var sc = fixture.Create<SomeClass>();
    
    // Perform test here...
    

    AutoFixture.Xunit 的情况与此相同,但现在没有客户端可以错误配置它。

    你是否觉得这样的设计合适取决于你,但我希望至少它是值得深思的。

    【讨论】:

    • 这是一个很好的答案。补充一点,我认为 SomeClass 实际上可能有 behavior 来设置这些,例如 Activate() 或 Expire() 方法。也许这些属性甚至不需要设置器!
    • +1 这应该是公认的答案。除了出色的设计反馈外,此答案中提供的 AutoFixture 示例比我的更简单。
    • 感谢您的详细解答!
    • +1 表示 TimeSpan 解决方案。但是,Postel 定律的解决方案对我来说似乎不直观。我不能完全解释为什么,但是构造函数中的这种逻辑感觉就像'不诚实'......我希望参数的顺序很重要,因为它们通常在编程中。也许如果这被抽象为具有静态工厂方法的专用Period 类,那么对有效的限制会更清楚。我会尝试将其放在答案中以证明我的意思
    • @AlexG 你可能不喜欢它,因为它太含蓄了,你认为explicit is better than implicit。老实说,在这种情况下,我倾向于同意你的看法,但我想勾勒出各种不同的解决方案,这也是我添加 Postel 定律解决方案的原因。
    【解决方案2】:

    这是对马克的回答的一种“扩展评论”,试图建立在他的 Postel 定律解决方案的基础上。构造函数中的参数交换让我感到不安,所以我在 Period 类中明确了日期交换行为。

    为简洁起见使用C#6 syntax

    public class Period
    {
        public DateTime Start { get; }
        public DateTime End { get; }
    
        public Period(DateTime start, DateTime end)
        {
            if (start > end) throw new ArgumentException("start should be before end");
            Start = start;
            End = end;
        }
    
        public static Period CreateSpanningDates(DateTime x, DateTime y, params DateTime[] others)
        {
            var all = others.Concat(new[] { x, y });
            var start = all.Min();
            var end = all.Max();
            return new Duration(start, end);
        }
    }
    
    public class SomeClass
    {
        public DateTime ValidFrom { get; }
        public DateTime ExpirationDate { get; }
    
        public SomeClass(Period period)
        {
            ValidFrom = period.Start;
            ExpirationDate = period.End;
        }
    }
    

    然后您需要为Period 定制您的夹具以使用静态构造函数:

    fixture.Customize<Period>(f =>
        f.FromFactory<DateTime, DateTime>((x, y) => Period.CreateSpanningDates(x, y)));
    

    我认为这个解决方案的主要好处是它将时间排序要求提取到它自己的类 (SRP) 中,并让您的业务逻辑以已经商定的合同的形式表达,从构造函数签名中可以明显看出。

    【讨论】:

      【解决方案3】:

      由于SomeClass 是可变的,这里有一种方法:

      [Fact]
      public void UsingGeneratorOfDateTime()
      {
          var fixture = new Fixture();
          var generator = fixture.Create<Generator<DateTime>>();
          var sut = fixture.Create<SomeClass>();
          var seed = fixture.Create<int>();
      
          sut.ExpirationDate =
              generator.First().AddYears(seed);
          sut.ValidFrom =
              generator.TakeWhile(dt => dt < sut.ExpirationDate).First();
      
          Assert.True(sut.ValidFrom < sut.ExpirationDate);
      }
      

      FWIW,使用AutoFixture with xUnit.net data theories,上面的测试可以写成:

      [Theory, AutoData]
      public void UsingGeneratorOfDateTimeDeclaratively(
          Generator<DateTime> generator,
          SomeClass sut,
          int seed)
      {
          sut.ExpirationDate =
              generator.First().AddYears(seed);
          sut.ValidFrom =
              generator.TakeWhile(dt => dt < sut.ExpirationDate).First();
      
          Assert.True(sut.ValidFrom < sut.ExpirationDate);
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-10-24
        • 2016-02-27
        • 2018-08-27
        • 1970-01-01
        • 1970-01-01
        • 2017-04-12
        • 1970-01-01
        • 2019-11-18
        相关资源
        最近更新 更多