Авторежим рефакторинга - PullRequest
10 голосов
/ 12 апреля 2010

Я начал использовать 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)));
}

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

1 Ответ

14 голосов
/ 12 апреля 2010

Ваша первоначальная попытка выглядит хорошо, но есть по крайней мере несколько вещей, которые вы можете немного упростить.

Прежде всего, вы сможете уменьшить это:

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

, что приведет к автоматическому разрешению автоматического исправления расписания. Этот подход более удобен в обслуживании, поскольку позволяет в будущем добавлять параметр в конструктор 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 для каждого вывоза и прибытия, но это ничем не отличается от того, что вы делали вручную в исходном тесте.

Когда дело доходит до времени, по умолчанию 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 объектов в моем примере выше, но из-за скорости ЦП последующие экземпляры DateTime, вероятно, будут идентичны.

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

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

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

public class DateTimeGenerator
{
    public DateTime Next();
}
...