Рефакторинг AutoFixture

Я начал использовать AutoFixture http://autofixture.codeplex.com/, так как мои модульные тесты были раздуты множеством настроек данных. Я потратил больше времени на настройку данных, чем на запись unit test. Вот пример того, как выглядит мой первоначальный unit test (пример, взятый из образца приложения для груза из синей книги 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)));
}

Я хотел бы знать, есть ли лучший способ реорганизовать его. Хотелось бы сделать это короче и легче.

Ответ 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. Тип inferencing будет определять, что вы регистрируете расписание из-за возвращаемого типа 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)

автообъект должен уметь самостоятельно разобраться.

Он создаст новый экземпляр местоположения для каждого location location location и arrivalLocation, но не отличается от того, что вы вручную сделали в оригинальном тесте.

Когда дело доходит до времени, по умолчанию 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, предоставление коллекции через любой член (включая конструктор) подразумевает, что члену разрешено изменять коллекцию (например, путем вызова методов "Добавить", "Удалить" и "Очистить" ). Это вряд ли поведение, которое вы ожидаете от конструктора, поэтому не допускайте его.

AutoFixture автоматически генерирует новые значения для всех объектов Location в моем примере выше, но из-за скорости процессора последующие экземпляры DateTime, вероятно, будут идентичными.

Если вы хотите увеличить DateTimes, вы можете написать небольшой класс, который увеличивает каждый возвращаемый DateTime каждый раз при его вызове. Я оставлю реализацию этого класса заинтересованному читателю, но вы могли бы зарегистрировать его так:

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

предполагая этот API (еще раз обратите внимание на синтаксис группы методов выше):

public class DateTimeGenerator
{
    public DateTime Next();
}