Многие объяснения уже присутствуют, чтобы объяснить, как это происходит и как это исправить, но вы также должны следовать рекомендациям, чтобы избежать NullPointerException
вообще.
См. также: A хороший список лучших практик
Я бы добавил, очень важно, хорошо использовать модификатор final
. Использование "окончательной" модификатор, когда это применимо в Java
Сводка:
final
для обеспечения хорошей инициализации. @NotNull
и @Nullable
if("knownObject".equals(unknownObject)
valueOf()
поверх toString (). StringUtils
StringUtils.isEmpty(null)
. Возможно, возникает соблазн задать вопрос о , как сделать 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 клиенты могут неправильно его настроить.
Независимо от того, находите ли вы, что такой дизайн подходит, зависит от вы, но я надеюсь, что, по крайней мере, это пища для размышлений.
Это своего рода «расширенный комментарий» в связи с ответом Марка, пытаясь опираться на его решение 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) и оставляет вашу бизнес-логику выраженной в терминах уже согласованного контракта, очевидного из подписи конструктора.
Поскольку 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);
}
Period
со статическим заводским методом, ограничения на достоверность были бы более ясными. Я попытаюсь поставить это в ответ, чтобы продемонстрировать, что я имею в виду – AlexFoxGill 23 February 2015 в 13:56