Подтвердить что ты не робот

Как использовать ограничения даты с помощью AutoFixture?

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

public class SomeClass
{
    public DateTime ValidFrom { get; set; }
    public DateTime ExpirationDate { get; set; }
}

Теперь я выполняю некоторые модульные тесты с помощью NUnit и использую AutoFixture для создания некоторых случайных данных:

[Test]
public void SomeTest()
{ 
    var fixture = new Fixture();
    var someRandom = fixture.Create<SomeClass>();
}

Это работает до сих пор. Но есть требование, чтобы дата ValidFrom всегда была ExpirationDate. Я должен обеспечить это, так как я выполняю некоторые положительные тесты.

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

У меня нет большого опыта работы с AutoFixture, но я знаю, что могу получить ICustomizationComposer, вызвав метод Build:

var fixture = new Fixture();
var someRandom = fixture.Build<SomeClass>()
    .With(some => /*some magic like some.ValidFrom < some.ExpirationDate here...*/ )
    .Create();

Может быть, это правильный способ достичь этого?

Заранее благодарим за помощь.

4b9b3361

Ответ 1

Возможно, возникает соблазн задать вопрос о том, как настроить AutoFixture на мой дизайн?, но часто более интересным может быть вопрос: как сделать мой дизайн более надежным?

Вы можете сохранить дизайн и "исправить" AutoFixture, но я не думаю, что это особенно хорошая идея.

Прежде чем я расскажу вам, как это сделать, в зависимости от ваших требований, возможно, все, что вам нужно сделать, это следующее.

Явное назначение

Почему бы просто не назначить допустимое значение ExpirationDate, например?

var sc = fixture.Create<SomeClass>();
sc.ExpirationDate = sc.ValidFrom + fixture.Create<TimeSpan>();

// Perform test here...

Если вы используете AutoFixture.Xunit, это может быть еще проще:

[Theory, AutoData]
public void ExplicitPostCreationFix_xunit(
    SomeClass sc,
    TimeSpan duration)
{
    sc.ExpirationDate = sc.ValidFrom + duration;

    // Perform test here...
}

Это довольно устойчиво, потому что хотя AutoFixture (IIRC) создает случайные значения TimeSpan, они останутся в положительном диапазоне, если вы не сделали что-то для своего fixture, чтобы изменить его поведение.

Этот подход будет самым простым способом решить ваш вопрос, если вам нужно протестировать SomeClass. С другой стороны, это не очень практично, если вам нужно SomeClass в качестве входных значений в мириадах других тестов.

В таких случаях может возникнуть соблазн исправить AutoFixture, что также возможно:

Изменение поведения AutoFixture

Теперь, когда вы видели, как решить проблему как одноразовое решение, вы можете сказать AutoFixture об этом как общее изменение способа создания SomeClass:

fixture.Customize<SomeClass>(c => c
    .Without(x => x.ValidFrom)
    .Without(x => x.ExpirationDate)
    .Do(x => 
        {
            x.ValidFrom = fixture.Create<DateTime>();
            x.ExpirationDate = 
                x.ValidFrom + fixture.Create<TimeSpan>();
        }));
// All sorts of other things can happen in between, and the
// statements above and below can happen in separate classes, as 
// long as the fixture instance is the same...
var sc = fixture.Create<SomeClass>();

Вы также можете упаковать вышеуказанный вызов до Customize в реализации ICustomization для дальнейшего повторного использования. Это также позволит вам использовать индивидуальный экземпляр fixture с AutoFixture.Xunit.

Измените дизайн SUT

В приведенных выше решениях описывается, как изменить поведение AutoFixture, AutoFixture была первоначально написана как инструмент TDD, а основной точкой TDD является предоставление обратной связи о системном тестировании (SUT). AutoFixture имеет тенденцию усиливать такую ​​обратную связь,, которая также имеет место здесь.

Рассмотрим конструкцию SomeClass. Ничто не мешает клиенту делать что-то вроде этого:

var sc = new SomeClass
{
    ValidFrom = new DateTime(2015, 2, 20),
    ExpirationDate = new DateTime(1900, 1, 1)
};

Это компилируется и выполняется без ошибок, но, вероятно, не то, что вы хотите. Таким образом, AutoFixture на самом деле не делает ничего плохого; SomeClass не защищает свои инварианты должным образом.

Это общая ошибка дизайна, где разработчики склонны слишком доверять семантической информации об именах участников. Кажется, что мышление заключается в том, что никто в здравом уме не установил бы ExpirationDate значение до ValidFrom! Проблема с таким аргументом заключается в том, что он предполагает, что все разработчики всегда будут присваивать эти значения парами.

Однако клиенты также могут получить экземпляр SomeClass, переданный им, и хотят обновить одно из значений, например:

sc.ExpirationDate = new DateTime(2015, 1, 31);

Это действительно? Как вы можете сказать?

Клиент может посмотреть sc.ValidFrom, но зачем это нужно? Вся цель инкапсуляции заключается в том, чтобы освободить клиентов от такого бремени.

Вместо этого вам следует рассмотреть возможность изменения дизайна SomeClass. Самое маленькое изменение дизайна, о котором я могу думать, это примерно так:

public class SomeClass
{
    public DateTime ValidFrom { get; set; }
    public TimeSpan Duration { get; set; }
    public DateTime ExpirationDate
    {
        get { return this.ValidFrom + this.Duration; }
    }
}

Это превращает ExpirationDate в свойство только для чтения. С этим изменением AutoFixture работает только из коробки:

var sc = fixture.Create<SomeClass>();

// Perform test here...

Вы также можете использовать его с помощью AutoFixture.Xunit:

[Theory, AutoData]
public void ItJustWorksWithAutoFixture_xunit(SomeClass sc)
{
    // Perform test here...
}

Это все еще немного хрупкое, потому что, хотя по умолчанию AutoFixture создает положительные значения TimeSpan, возможно также изменить это поведение.

Кроме того, дизайн фактически позволяет клиентам назначать отрицательные значения TimeSpan для свойства Duration:

sc.Duration = TimeSpan.FromHours(-1);

Должно ли это разрешаться, зависит от модели домена. Как только вы начнете рассматривать эту возможность, на самом деле может оказаться, что определение периодов времени, которые движутся назад во времени, действительно в домене...

Дизайн в соответствии с законом Postel

Если проблемный домен - это тот, где возвращение назад во времени не разрешено, вы можете подумать о добавлении предложения Guard в свойство Duration, отклоняя отрицательные промежутки времени.

Однако лично я часто нахожу, что я прихожу к лучшему дизайну API, когда серьезно отношусь к

Ответ 2

Это своего рода "расширенный комментарий" в отношении ответа Марка, пытаясь опираться на его решение Postel Law. Перестановка параметров в конструкторе мне показалась для меня непростой, поэтому я сделал переопределение даты явным в классе Period.

Использование Синтаксис С# 6 для краткости:

public class Period
{
    public DateTime Start { get; }
    public DateTime End { get; }

    public Period(DateTime start, DateTime end)
    {
        if (start > end) throw new ArgumentException("start should be before end");
        Start = start;
        End = end;
    }

    public static Period CreateSpanningDates(DateTime x, DateTime y, params DateTime[] others)
    {
        var all = others.Concat(new[] { x, y });
        var start = all.Min();
        var end = all.Max();
        return new Duration(start, end);
    }
}

public class SomeClass
{
    public DateTime ValidFrom { get; }
    public DateTime ExpirationDate { get; }

    public SomeClass(Period period)
    {
        ValidFrom = period.Start;
        ExpirationDate = period.End;
    }
}

Затем вам нужно настроить свой прибор для Period для использования статического конструктора:

fixture.Customize<Period>(f =>
    f.FromFactory<DateTime, DateTime>((x, y) => Period.CreateSpanningDates(x, y)));

Я думаю, что основное преимущество этого решения заключается в том, что он извлекает требование о временном порядке в свой собственный класс (SRP) и оставляет вашу бизнес-логику выраженной в терминах уже согласованного контракта, очевидного из подписи конструктора.

Ответ 3

Так как SomeClass является изменяемым, здесь один из способов сделать это:

[Fact]
public void UsingGeneratorOfDateTime()
{
    var fixture = new Fixture();
    var generator = fixture.Create<Generator<DateTime>>();
    var sut = fixture.Create<SomeClass>();
    var seed = fixture.Create<int>();

    sut.ExpirationDate =
        generator.First().AddYears(seed);
    sut.ValidFrom =
        generator.TakeWhile(dt => dt < sut.ExpirationDate).First();

    Assert.True(sut.ValidFrom < sut.ExpirationDate);
}

FWIW, используя AutoFixture с теориями данных xUnit.net, вышеуказанный тест можно записать как:

[Theory, AutoData]
public void UsingGeneratorOfDateTimeDeclaratively(
    Generator<DateTime> generator,
    SomeClass sut,
    int seed)
{
    sut.ExpirationDate =
        generator.First().AddYears(seed);
    sut.ValidFrom =
        generator.TakeWhile(dt => dt < sut.ExpirationDate).First();

    Assert.True(sut.ValidFrom < sut.ExpirationDate);
}