Используйте AutoFixture для создания частичных случайных значений для свойств и зависимостей значений между свойствами [duplicate]

Многие объяснения уже присутствуют, чтобы объяснить, как это происходит и как это исправить, но вы также должны следовать рекомендациям, чтобы избежать NullPointerException вообще.

См. также: A хороший список лучших практик

Я бы добавил, очень важно, хорошо использовать модификатор final. Использование "окончательной" модификатор, когда это применимо в Java

Сводка:

  1. Используйте модификатор final для обеспечения хорошей инициализации.
  2. Избегайте возврата null в методы, например, при возврате пустых коллекций.
  3. Использовать аннотации @NotNull и @Nullable
  4. Быстрое завершение работы и использование утверждений, чтобы избежать распространения нулевых объектов через все приложение, когда они не должен быть пустым.
  5. Сначала используйте значения с известным объектом: if("knownObject".equals(unknownObject)
  6. Предпочитают valueOf() поверх toString ().
  7. Используйте null safe StringUtils StringUtils.isEmpty(null).

12
задан oopbase 19 February 2015 в 11:21
поделиться

3 ответа

Возможно, возникает соблазн задать вопрос о , как сделать 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);

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

Дизайн согласно Закону Постела

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

Однако, лично я часто обнаруживаю, что я прихожу к лучшему дизайну API, когда серьезно отношусь к закону Postel's . В этом случае почему бы не изменить дизайн так, чтобы SomeClass всегда использовал абсолютный TimeSpan вместо подписанного TimeSpan?

. В этом случае я бы предпочел неизменный объект, который не применяет роли двух экземпляров DateTime до тех пор, пока он не узнает их значения:

public class SomeClass
{
    private readonly DateTime validFrom;
    private readonly DateTime expirationDate;

    public SomeClass(DateTime x, DateTime y)
    {
        if (x < y)
        {
            this.validFrom = x;
            this.expirationDate = y;
        }
        else
        {
            this.validFrom = y;
            this.expirationDate = x;
        }
    }

    public DateTime ValidFrom
    {
        get { return this.validFrom; }
    }

    public DateTime ExpirationDate
    {
        get { return this.expirationDate; }
    }
}

Как и предыдущая редизайн, этот просто работает из коробки с помощью AutoFixture:

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

// Perform test here...

То же самое происходит с AutoFixture.Xunit, но теперь no клиенты могут неправильно его настроить.

Независимо от того, находите ли вы, что такой дизайн подходит, зависит от вы, но я надеюсь, что, по крайней мере, это пища для размышлений.

26
ответ дан Mark Seemann 19 August 2018 в 18:49
поделиться
  • 1
    Это отличный ответ. Чтобы добавить к этому, я полагаю, что возможно, что SomeClass имеет поведение , которое устанавливает их, например, методы Activate () или Expire (). Возможно, этим свойствам даже не нужны сеттеры! – Dave Nichol 20 February 2015 в 18:10
  • 2
    +1 Это должен быть принятый ответ. Помимо отличной обратной связи с дизайном, примеры AutoFixture, представленные в этом ответе, проще, чем у меня. – Nikos Baxevanis 20 February 2015 в 19:56
  • 3
    Спасибо за этот подробный ответ! – oopbase 21 February 2015 в 13:53
  • 4
    +1 для решения TimeSpan. Однако решение Postel's Law кажется мне неинтуитивным. Я не могу сказать, почему, но такая логика в конструкторе кажется «нечестностью» ... Я бы ожидал, что порядок параметров будет важен, как это обычно бывает в программировании. Возможно, если бы это было абстрагировано с выделенным классом Period со статическим заводским методом, ограничения на достоверность были бы более ясными. Я попытаюсь поставить это в ответ, чтобы продемонстрировать, что я имею в виду – AlexFoxGill 23 February 2015 в 13:56
  • 5
    @AlexG Возможно, вам это не нравится, потому что это слишком неявно, и вы считаете, что явный лучше, чем неявный . Честно говоря, я бы склонен согласиться с вами в этом случае, но я хотел набросать различные решения, поэтому я также добавил решение Postel's Law. – Mark Seemann 23 February 2015 в 14:31

Это своего рода «расширенный комментарий» в связи с ответом Марка, пытаясь опираться на его решение Law Postel's Law. Параметр, меняющийся в конструкторе, чувствовал себя неловко для меня, поэтому я сделал переопределение даты в явном виде в классе Period.

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

4
ответ дан AlexFoxGill 19 August 2018 в 18:49
поделиться

Поскольку 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);
}
3
ответ дан Nikos Baxevanis 19 August 2018 в 18:49
поделиться
Другие вопросы по тегам:

Похожие вопросы: