代码之家  ›  专栏  ›  技术社区  ›  Tomasz Jaskuλa

自动修复重构

  •  10
  • Tomasz Jaskuλa  · 技术社区  · 15 年前

    我开始使用自动装配 http://autofixture.codeplex.com/ 因为我的单元测试由于大量的数据设置而膨胀。我花在设置数据上的时间比写单元测试的时间多。下面是我的初始单元测试的一个示例(示例取自DDD Blue Book中的Cargo应用程序示例)

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

    我想知道是否有更好的方法来重构它。想做的更短更容易。

    1 回复  |  直到 8 年前
        1
  •  14
  •   Mark Seemann    15 年前

    你最初的尝试看起来不错,但至少有一些事情你可以简化一点。

    首先,您应该能够减少:

    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的返回类型,您正在注册计划。

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

    public Schedule(IEnumerable<CarrierMovement> carrierMovements)
    

    你可以直接注册 carrierMovements 这样地:

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

    这将导致自动修复自动解决正确的时间表。这种方法更易于维护,因为它允许您将来在不破坏测试的情况下向调度构造函数添加参数(只要autofixture可以解析参数类型)。

    但是,在这种情况下,我们可以做得更好,因为我们没有真正使用 搬运 变量,用于注册以外的任何内容。我们真正需要做的只是告诉Autofixture如何创建 IEnumerable<CarrierMovement> . 如果您不关心数字50(您不应该关心),我们甚至可以使用如下方法组语法:

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

    注意缺少方法调用paranthess:我们注册了一个func,自从 CreateMany<T> 方法返回 IEnumerable<T> 类型推断处理剩下的。

    然而,这些都是细节。在更高的层次上,您可能会考虑根本不注册carriermovement。假设此构造函数:

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

    自动修复应该能够自己解决。

    它将为每个出发地点和到达地点创建一个新的地点实例,但这与您在原始测试中手动执行的操作没有区别。

    当涉及到时间时,默认情况下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设计的角度来看,通过任何成员(包括构造函数)提供集合意味着允许该成员修改集合(例如,通过调用其添加、删除和清除方法)。这几乎不是您期望从构造函数得到的行为,所以不要允许这样做。

    Autofixture将自动为所有 Location 上面例子中的对象,但是由于CPU的速度,日期时间的后续实例可能是相同的。

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

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

    假设使用此API(请再次注意上面的方法组语法):

    public class DateTimeGenerator
    {
        public DateTime Next();
    }