2010-04-12 42 views
10

我开始使用AutoFixture http://autofixture.codeplex.com/,因为我的单元测试臃肿了很多数据设置。我花更多时间来完成数据,而不是编写单元测试。下面是我最初的单元测试的样子(从货物申请样品从DDD蓝皮书所采取的示例)AutoFixture重构

[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))); 
} 

我想知道是否有更好的方法来重构它。想要做到这一点更短,更容易。

回答

14

您的初次尝试看起来不错,但至少有一些事情可以简化一下。

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

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。

但是,假设附表构造是这样的:

public Schedule(IEnumerable<CarrierMovement> carrierMovements) 

你可以代替刚刚注册的carrierMovements这样的:

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

这将导致AutoFixture正确地自动解决时间表。这种方法更具可维护性,因为它允许您在将来添加参数到Schedule构造函数而不会中断测试(只要AutoFixture可以解析参数类型)。

但是,在这种情况下,我们可以做得比这更好,因为我们实际上并没有使用carrierMovements变量来完成注册以外的任何操作。我们真正需要做的只是告诉AutoFixture如何创建IEnumerable<CarrierMovement>的实例。如果你不关心50号(你应该),我们甚至可以使用方法组的语法是这样的:

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

通知缺少方法调用括号的:我们要注册一个FUNC键和因为CreateMany<T>方法返回IEnumerable<T>类型推理负责其余部分。

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

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

autofixture应该能够找出它自己。

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

说到时间,默认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<CarrierMovement>的问题,您将需要注册。下面是做这件事:

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

不过,既然你问了,我暗示附表构造是这样的:

public Schedule(IList<CarrierMovement> carrierMovements) 

,我真的觉得你应该重新考虑改变这种API采取的IEnumerable<Carriemovement>。从API设计角度来看,通过任何成员(包括构造函数)提供集合意味着成员可以修改集合(例如通过调用它的Add,Remove和Clear方法)。这几乎不是你期望从构造函数中获得的行为,所以不要允许它。

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

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

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

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

public class DateTimeGenerator 
{ 
    public DateTime Next(); 
} 
+0

感谢您的意见。不过,我有一个AutoFixture抛出的小例外 Ploeh.AutoFixture.ObjectCreationException:AutoFixture无法创建System.Collections.Generic.IList'1 [DDDBookingApplication.Domain.Voyage.CarrierMovement]类型的实例,因为它没有公开构造函数。 我认为我应该告诉如何创建CarrierMovement? – 2010-04-12 14:09:47

+0

我也想为所有instaces有不同的数据集。你的想法是什么? – 2010-04-12 14:11:51

+0

感谢您的所有细节。现在测试很短,并通过:) – 2010-04-12 15:11:18