如何使用AutoFixture实现日期限制?

oop*_*ase 13 c# autofixture

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

public class SomeClass
{
    public DateTime ValidFrom { get; set; }
    public DateTime ExpirationDate { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

那么使用AutoFixture 有一种简单的方法来实现它吗?我知道我可以创建一个修复日期并添加一个随机日期间隔来解决这个问题,但如果AutoFixture可以自己处理这个要求,那将会很棒.

我没有很多使用AutoFixture的经验,但我知道我可以ICustomizationComposer通过调用Build方法得到一个:

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

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

在此先感谢您的帮助.

Mar*_*ann 34

提问如何使AutoFixture适应我的设计可能很诱人但通常,一个更有趣的问题可能是:如何让我的设计更加健壮?

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

在我告诉你如何做到这一点之前,根据你的要求,你可能需要做的就是以下几点.

明确的分配

为什么不简单地指定一个有效值ExpirationDate,像这样?

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

// Perform test here...
Run Code Online (Sandbox Code Playgroud)

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

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

    // Perform test here...
}
Run Code Online (Sandbox Code Playgroud)

这是相当强大的,因为即使AutoFixture(IIRC)创建随机TimeSpan值,它们也将保持在正范围内,除非你已经做了一些事情fixture来改变它的行为.

如果您需要进行SomeClass自我测试,这种方法将是解决您问题的最简单方法.另一方面,如果你需要SomeClass在无数其他测试中作为输入值,它就不太实用.

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

更改AutoFixture的行为

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

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>();
Run Code Online (Sandbox Code Playgroud)

您也可以打包上述呼吁CustomizeICustomization实施,进一步重用.这还可以使您使用FixtureAutoFixture.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)
};
Run Code Online (Sandbox Code Playgroud)

这编译并运行没有错误,但可能不是你想要的.因此,AutoFixture实际上没有做错任何事情; SomeClass没有正确保护其不变量.

这是一个常见的设计错误,开发人员倾向于过多地信任成员名称的语义信息.这种想法似乎是在他们正确的思想中没有人会在之前设定ExpirationDate一个价值!这种论点的问题在于它假设所有开发人员都将成对地分配这些值. ValidFrom

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

sc.ExpirationDate = new DateTime(2015, 1, 31);
Run Code Online (Sandbox Code Playgroud)

这有效吗?你怎么知道?

客户可以看一下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; }
    }
}
Run Code Online (Sandbox Code Playgroud)

这变成ExpirationDate只读的计算属性.通过此更改,AutoFixture开箱即用:

var sc = fixture.Create<SomeClass>();

// Perform test here...
Run Code Online (Sandbox Code Playgroud)

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

[Theory, AutoData]
public void ItJustWorksWithAutoFixture_xunit(SomeClass sc)
{
    // Perform test here...
}
Run Code Online (Sandbox Code Playgroud)

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

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

sc.Duration = TimeSpan.FromHours(-1);
Run Code Online (Sandbox Code Playgroud)

是否允许这取决于域模型.一旦你开始考虑这种可能性,实际上可能会发现在时间上向后移动的定义时间段在域中是有效的...

根据Postel定律设计

如果问题域是不允许回溯的域,则可以考虑向Duration属性添加Guard子句,拒绝负时间跨度.

但是,就个人而言,我经常发现,当我认真对待Postel法则时,我会得到更好的API设计.在这种情况下,为什么不改变设计,以便SomeClass始终使用绝对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; }
    }
}
Run Code Online (Sandbox Code Playgroud)

与之前的重新设计一样,这只是 AutoFixture的开箱即用:

var sc = fixture.Create<SomeClass>();

// Perform test here...
Run Code Online (Sandbox Code Playgroud)

AutoFixture.Xunit的情况也是如此,但现在没有客户端可能会错误配置它.

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

  • 这是一个很好的答案.只是为了添加它,我认为SomeClass可能实际上具有设置这些的*行为*,例如Activate()或Expire()方法.也许这些属性甚至不需要制定者! (3认同)
  • @AlexG您可能不喜欢它,因为它太隐式了,并且您认为[显式优于隐式](https://www.python.org/dev/peps/pep-0020)。老实说,在这种情况下我倾向于同意你的观点,但我想勾勒出各种不同的解决方案,这就是我还添加了波斯特尔定律解决方案的原因。 (2认同)

Ale*_*ill 5

这是一种关于马克答案的"扩展评论",试图以他的Postel法律解决方案为基础.构造函数中的参数交换对我来说感到不安,所以我在Period类中明确地交换了日期交换行为.

使用C#6语法简洁:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后,您需要自定义夹具Period以使用静态构造函数:

fixture.Customize<Period>(f =>
    f.FromFactory<DateTime, DateTime>((x, y) => Period.CreateSpanningDates(x, y)));
Run Code Online (Sandbox Code Playgroud)

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