AutoFixture重构

Tom*_*uλa 10 c# unit-testing autofixture

我开始使用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();
}
Run Code Online (Sandbox Code Playgroud)

以下是我尝试使用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)));
}
Run Code Online (Sandbox Code Playgroud)

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

Mar*_*ann 14

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

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

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

对此:

fixture.Register<Location, Location, DateTime, DateTime, CarrierMovement>(
    () => new CarrierMovement(departureLoc, arrivalLoc, departureDateTime, arrivalDateTime));
Run Code Online (Sandbox Code Playgroud)

因为你没有使用那些其他变量.但是,这实际上锁定了CarrierMovement的任何创建以使用相同的四个值.虽然每个创建的CarrierMovement都是一个单独的实例,但它们都将共享相同的四个值,我想知道这是不是你的意思?

与上述相同,而不是

fixture.Register<List<CarrierMovement>, Schedule>((carrierM) =>
    new Schedule(carrierMovements));
Run Code Online (Sandbox Code Playgroud)

你可以写

fixture.Register(() => new Schedule(carrierMovements));
Run Code Online (Sandbox Code Playgroud)

因为你不使用carrierM变量.由于Func的返回类型,类型推断将确定您正在注册一个Schedule.

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

public Schedule(IEnumerable<CarrierMovement> carrierMovements)
Run Code Online (Sandbox Code Playgroud)

你可以改为注册carrierMovements这样的:

fixture.Register<IEnumerable<CarrierMovement>>(carrierMovements);
Run Code Online (Sandbox Code Playgroud)

这将导致AutoFixture正确自动解决Schedule.这种方法更易于维护,因为它允许您在未打破测试的情况下将参数添加到Schedule构造函数中(只要AutoFixture可以解析参数类型).

但是,在这种情况下,我们可以做得更好,因为carrierMovements除了注册之外我们并没有真正使用变量.我们真正需要做的只是告诉AutoFixture如何创建实例IEnumerable<CarrierMovement>.如果你不关心数字50(你不应该),我们甚至可以使用像这样的方法组语法:

fixture.Register(fixture.CreateMany<CarrierMovement>);
Run Code Online (Sandbox Code Playgroud)

注意缺少方法调用parantheses:我们正在注册一个Func,并且由于该CreateMany<T>方法返回IEnumerable<T>类型推理负责其余的.

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

public CarrierMovement(Location departureLocation,
    Location arrivalLocation,
    DateTime departureTime,
    DateTime arrivalTime)
Run Code Online (Sandbox Code Playgroud)

自动混合应该能够自己弄清楚.

它将为每个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();
}
Run Code Online (Sandbox Code Playgroud)

要解决此问题,IList<CarrierMovement>您需要注册它.这是一种方法:

fixture.Register<IList<CarrierMovement>>(() =>
    fixture.CreateMany<CarrierMovement>().ToList());
Run Code Online (Sandbox Code Playgroud)

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

public Schedule(IList<CarrierMovement> carrierMovements)
Run Code Online (Sandbox Code Playgroud)

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

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

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

var dtg = new DateTimeGenerator();
fixture.Register(dtg.Next);
Run Code Online (Sandbox Code Playgroud)

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

public class DateTimeGenerator
{
    public DateTime Next();
}
Run Code Online (Sandbox Code Playgroud)