10

我开始使用 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)));
}

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

4

1 回答 1

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。

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

public Schedule(IEnumerable<CarrierMovement> carrierMovements)

相反,您可以carrierMovements像这样注册:

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

这将导致 AutoFixture 自动正确解析 Schedule。这种方法更易于维护,因为它允许您将来在不破坏测试的情况下向 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 应该能够自己解决。

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

说到时间,默认情况下 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());

但是,既然你问了,我暗示 Schedule 构造函数如下所示:

public Schedule(IList<CarrierMovement> carrierMovements)

而且我真的认为您应该重新考虑更改该 API 以采用IEnumerable<Carriemovement>. 从 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();
}
于 2010-04-12T13:54:52.513 回答