Ответ 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, когда серьезно отношусь к