【问题标题】:AutoFixture refactoringAutoFixture 重构
【发布时间】:2010-04-12 13:22:22
【问题描述】:

我开始使用 AutoFixture http://autofixture.codeplex.com/,因为我的单元测试因大量数据设置而变得臃肿。我花在设置数据上的时间比编写单元测试要多。这是我的初始单元测试的示例(示例来自 DDD 蓝皮书的货物应用程序示例)

[Test]
public void should_create_instance_with_correct_ctor_parameters()
{
    var carrierMovements = new List<CarrierMovement>();

    var deparureUnLocode1 = new UnLocode("AB44D");
    var departureLocation1 = new Location(deparureUnLocode1, "HAMBOURG");
    var arrivalUnLocode1 = new UnLocode("XX44D");
    var arrivalLocation1 = new Location(arrivalUnLocode1, "TUNIS");
    var departureDate1 = new DateTime(2010, 3, 15);
    var arrivalDate1 = new DateTime(2010, 5, 12);

    var carrierMovement1 = new CarrierMovement(departureLocation1, arrivalLocation1, departureDate1, arrivalDate1);

    var deparureUnLocode2 = new UnLocode("CXRET");
    var departureLocation2 = new Location(deparureUnLocode2, "GDANSK");
    var arrivalUnLocode2 = new UnLocode("ZEZD4");
    var arrivalLocation2 = new Location(arrivalUnLocode2, "LE HAVRE");
    var departureDate2 = new DateTime(2010, 3, 18);
    var arrivalDate2 = new DateTime(2010, 3, 31);

    var carrierMovement2 = new CarrierMovement(departureLocation2, arrivalLocation2, departureDate2, arrivalDate2);

    carrierMovements.Add(carrierMovement1);
    carrierMovements.Add(carrierMovement2);

    new Schedule(carrierMovements).ShouldNotBeNull();
}

这是我尝试使用 AutoFixture 重构它的方法

[Test]
public void should_create_instance_with_correct_ctor_parameters_AutoFixture()
{
    var fixture = new Fixture();

    fixture.Register(() => new UnLocode(UnLocodeString()));

    var departureLoc = fixture.CreateAnonymous<Location>();
    var arrivalLoc = fixture.CreateAnonymous<Location>();
    var departureDateTime = fixture.CreateAnonymous<DateTime>();
    var arrivalDateTime = fixture.CreateAnonymous<DateTime>();

    fixture.Register<Location, Location, DateTime, DateTime, CarrierMovement>(
        (departure, arrival, departureTime, arrivalTime) => new CarrierMovement(departureLoc, arrivalLoc, departureDateTime, arrivalDateTime));

    var carrierMovements = fixture.CreateMany<CarrierMovement>(50).ToList();

    fixture.Register<List<CarrierMovement>, Schedule>((carrierM) => new Schedule(carrierMovements));

    var schedule = fixture.CreateAnonymous<Schedule>();

    schedule.ShouldNotBeNull();
}

private static string UnLocodeString()
{
    var stringBuilder = new StringBuilder();

    for (int i = 0; i < 5; i++)
        stringBuilder.Append(GetRandomUpperCaseCharacter(i));

    return stringBuilder.ToString();
}

private static char GetRandomUpperCaseCharacter(int seed)
{
    return ((char)((short)'A' + new Random(seed).Next(26)));
}

我想知道是否有更好的重构方法。希望做得更短更容易。

【问题讨论】:

    标签: c# unit-testing autofixture


    【解决方案1】:

    您最初的尝试看起来不错,但至少有几件事可以简化一下。

    首先,你应该能够减少这个:

    fixture.Register<Location, Location, DateTime, DateTime, CarrierMovement>(
        (departure, arrival, departureTime, arrivalTime) =>
            new CarrierMovement(departureLoc, arrivalLoc, departureDateTime, arrivalDateTime));
    

    到这里:

    fixture.Register<Location, Location, DateTime, DateTime, CarrierMovement>(
        () => new CarrierMovement(departureLoc, arrivalLoc, departureDateTime, arrivalDateTime));
    

    因为您没有使用其他变量。但是,这实质上会锁定 CarrierMovement 的任何创建以使用相同的四个值。尽管每个创建的 CarrierMovement 都会是一个单独的实例,但它们都将共享相同的四个值,我想知道这是否是您的意思?

    与上述相同,而不是

    fixture.Register<List<CarrierMovement>, Schedule>((carrierM) =>
        new Schedule(carrierMovements));
    

    你可以写

    fixture.Register(() => new Schedule(carrierMovements));
    

    因为您不使用 carrierM 变量。由于 Func 的返回类型,类型推断会发现您正在注册一个 Schedule。

    但是,假设 Schedule 构造函数如下所示:

    public Schedule(IEnumerable<CarrierMovement> carrierMovements)
    

    您也可以像这样注册carrierMovements

    fixture.Register<IEnumerable<CarrierMovement>>(carrierMovements);
    

    这将导致 AutoFixture 自动正确解析 Schedule。这种方法更易于维护,因为它允许您在将来向 Schedule 构造函数添加参数而不会破坏测试(只要 AutoFixture 可以解析参数类型)。

    但是,在这种情况下,我们可以做得更好,因为除了注册之外,我们并没有真正将 carrierMovements 变量用于其他任何事情。我们真正需要做的只是告诉 AutoFixture 如何创建IEnumerable&lt;CarrierMovement&gt; 的实例。如果你不关心数字 50(你不应该),我们甚至可以像这样使用 Method Group 语法:

    fixture.Register(fixture.CreateMany<CarrierMovement>);
    

    请注意缺少方法调用括号:我们正在注册一个 Func,并且由于 CreateMany&lt;T&gt; 方法返回 IEnumerable&lt;T&gt;,因此类型推断会负责其余的工作。

    不过,这些都是细节。在更高层次上,您可能需要考虑根本不注册 CarrierMovement。假设这个构造函数:

    public CarrierMovement(Location departureLocation,
        Location arrivalLocation,
        DateTime departureTime,
        DateTime arrivalTime)
    

    autofixture 应该可以自己解决。

    它将为每个离开位置和到达位置创建一个新的位置实例,但这与您在原始测试中手动执行的操作没有什么不同。

    说到时间,AutoFixture 默认使用DateTime.Now,这至少保证了到达时间永远不会早于出发时间。但是,它们很可能是相同的,但如果有问题,您总是可以注册一个自动递增函数。

    考虑到这些因素,这里有一个替代方案:

    public void should_create_instance_with_correct_ctor_parameters_AutoFixture()
    {
        var fixture = new Fixture();
    
        fixture.Register(() => new UnLocode(UnLocodeString()));
    
        fixture.Register(fixture.CreateMany<CarrierMovement>);
    
        var schedule = fixture.CreateAnonymous<Schedule>();
    
        schedule.ShouldNotBeNull();
    }
    

    要解决IList&lt;CarrierMovement&gt; 的问题,您需要注册它。这是一种方法:

    fixture.Register<IList<CarrierMovement>>(() =>
        fixture.CreateMany<CarrierMovement>().ToList());
    

    但是,既然你问了,我暗示 Schedule 构造函数看起来像这样:

    public Schedule(IList<CarrierMovement> carrierMovements)
    

    我真的认为您应该重新考虑更改该 API 以采用 IEnumerable&lt;Carriemovement&gt;。从 API 设计的角度来看,通过任何成员(包括构造函数)提供集合意味着允许该成员修改集合(例如,通过调用它的 Add、Remove 和 Clear 方法)。这几乎不是您对构造函数所期望的行为,所以不要允许它。

    AutoFixture 将自动为我上面的示例中的所有 Location 对象生成新值,但由于 CPU 的速度,DateTime 的后续实例可能是相同的。

    如果您想增加 DateTimes,您可以编写一个小类,每次调用时都会增加返回的 DateTime。我将把那个类的实现留给感兴趣的读者,但你可以像这样注册它:

    var dtg = new DateTimeGenerator();
    fixture.Register(dtg.Next);
    

    假设这个 API(再次注意上面的方法组语法):

    public class DateTimeGenerator
    {
        public DateTime Next();
    }
    

    【讨论】:

    • 感谢您的 cmets。但是我有一个由 AutoFixture Ploeh.AutoFixture.ObjectCreationException 引发的小异常:AutoFixture 无法创建 System.Collections.Generic.IList`1[DDDBookingApplication.Domain.Voyage.CarrierMovement] 类型的实例,因为它没有公共构造函数。我假设我应该告诉如何创建 CarrierMovement ?
    • 我还想为所有实例提供不同的数据集。你有什么想法?
    • 感谢您提供所有详细信息。现在测试很短并且通过了:)