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

标签 c# autofixture

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

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的丰富经验,但是我知道我可以通过调用ICustomizationComposer方法获得Build:
var fixture = new Fixture();
var someRandom = fixture.Build<SomeClass>()
    .With(some => /*some magic like some.ValidFrom < some.ExpirationDate here...*/ )
    .Create();

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

在此先感谢您的帮助。

最佳答案

可能会很想问一个问题:如何使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设置为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法则设计

如果问题域是不允许返回时间域的域,则可以考虑将Guard子句添加到Duration属性中,从而拒绝负的时间跨度。

但是,就我个人而言,当我认真对待Postel's Law时,经常会发现我得到了更好的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; }
    }
}

像以前的重新设计一样,这对于AutoFixture来说是开箱即用的:
var sc = fixture.Create<SomeClass>();

// Perform test here...

AutoFixture.Xunit的情况相同,但是现在没有客户端可以对其进行错误配置。

是否找到合适的设计取决于您,但我希望至少这是值得深思的。

关于c# - 如何使用AutoFixture实现日期限制?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28603831/

相关文章:

interface - AutoMoqCustomization 适用于抽象类吗?

unit-testing - 为什么 AutoFixture 自定义会导致未填充继承的属性?

c# - IList<something> 构造函数参数和 AutoFixture

c# - 带二维数组的内存流

c# - CheckBoxFor 选中时未正确注册

c# - 在 MVVM 中传递服务

c# - 这种情况下如何使用AutoFixture呢?

c# - 这种将异步方法转换为同步方法的方法是否正确?

javascript - 将嵌套的 c# 类型转换为 json 并在 javascript 中使用它

c# - 使用 Autofixture 生成副本