Модульный тест для проверки создания объекта домена

11

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

[Test]
public void Should_create_person()
{
     Assert.DoesNotThrow(() => new Person(Guid.NewGuid(), new DateTime(1972, 01, 01));
}

Я утверждаю, что здесь создается объект Person, т. Е. Проверка не завершается неудачей. Например, если Guid имеет значение null или дата рождения ранее, чем 01.01.1900, то проверка не будет выполнена, и будет выдано исключение (что означает, что тест не пройден).

Конструктор выглядит так:

public Person(Id id, DateTime dateOfBirth) :
        base(id)
    {
        if (dateOfBirth == null)
            throw new ArgumentNullException("Date of Birth");
        elseif (dateOfBith < new DateTime(1900,01,01)
            throw new ArgumentException("Date of Birth");
        DateOfBirth = dateOfBirth;
    }

Это хорошая идея для теста?

Примечание . Я следую классическому подходу к модульному тестированию модели предметной области, если это имеет какое-либо отношение.

w0051977
источник
Есть ли в конструкторе какая-либо логика, которую стоит утверждать после инициализации?
Laiv
2
Никогда не беспокойтесь о тестировании конструкторов !!! Строительство должно идти прямо вперед. Вы ожидаете сбои в Guid.NewGuid () или конструкторе DateTime?
Ivenxu
@Laiv, пожалуйста, смотрите обновление к вопросу.
w0051977
1
Не стоит реализовывать тест как тот, которым вы поделились. Однако я бы проверил и обратное. Я бы протестировал случай, когда birthDate вызывает ошибку. Это инвариант класса, который вы хотите контролировать и тестировать.
Laiv
3
Тест выглядит хорошо, за исключением одного: имени. Should_create_person? Что должен создать человек? Дайте ему значимое имя, как Creating_person_with_valid_data_succeeds.
Дэвид Арно

Ответы:

18

Это действительный тест (хотя и довольно усердный), и я иногда делаю это для проверки логики конструктора, однако, как упоминал Лаив в комментариях, вы должны спросить себя, почему.

Если ваш конструктор выглядит так:

public Person(Guid guid, DateTime dob)
{
  this.Guid = guid;
  this.Dob = dob;
}

Много ли смысла в тестировании бросает? Правильно ли назначены параметры, я могу понять, но ваш тест довольно излишний.

Однако, если ваш тест делает что-то вроде этого:

public Person(Guid guid, DateTime dob)
{
  if(guid == default(Guid)) throw new ArgumentException("Guid is invalid");
  if(dob == default(DateTime)) throw new ArgumentException("Dob is invalid");

  this.Guid = guid;
  this.Dob = dob;
}

Тогда ваш тест становится более актуальным (поскольку вы на самом деле генерируете исключения где-то в коде).

Одна вещь, я бы сказал, как правило, плохая практика - иметь много логики в конструкторе. Базовая проверка (например, проверки null / default, которые я делаю выше) в порядке. Но если вы подключаетесь к базам данных и загружаете чьи-то данные, тогда код начинает пахнуть ...

Из-за этого, если ваш конструктор стоит протестировать (потому что в нем много логики), возможно, что-то еще не так.

Вы почти наверняка будете иметь другие тесты, охватывающие этот класс на уровнях бизнес-логики, конструкторы и назначения переменных почти наверняка получат полное покрытие из этих тестов. Поэтому может быть бессмысленно добавлять специальные тесты специально для конструктора. Тем не менее, ничто не является черно-белым, и я бы ничего не имел против этих тестов, если бы я проверял их код - но я бы задал вопрос, добавляют ли они большую ценность сверх тестов где-либо еще в вашем решении.

В вашем примере:

public Person(Id id, DateTime dateOfBirth) :
        base(id)
    {
        if (dateOfBirth == null)
            throw new ArgumentNullException("Date of Birth");
        elseif (dateOfBith < new DateTime(1900,01,01)
            throw new ArgumentException("Date of Birth");
        DateOfBirth = dateOfBirth;
    }

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

TLDR

Эти тесты имеют определенную ценность, однако логика проверки / назначения, вероятно, будет охватываться другими тестами в вашем решении. Если в этих конструкторах много логики, которая требует значительного тестирования, то это наводит меня на мысль о неприятном запахе кода.

Liath
источник
@Laith, смотрите обновление моего вопроса
w0051977
Я заметил, что вы вызываете базовый конструктор в вашем примере. ИМХО это добавляет ценность вашему тесту, логика конструктора теперь разделена на два класса и, следовательно, риск изменения немного выше, что дает больше оснований для его тестирования.
Лиат
«Однако, если ваш тест делает что-то вроде этого:« Разве вы не имеете в виду «если ваш конструктор делает что-то подобное» ?
Кодос Джонсон
«В этих тестах есть некоторая ценность» - интересно, во всяком случае, для меня значение показывает, что мы могли бы сделать этот тест избыточным, используя новый класс для представления урока (например PersonBirthdate), который выполняет проверку даты рождения. Аналогичным образом Guidпроверка может быть реализована в Idклассе. Это означает, что вам действительно больше не нужно иметь эту логику проверки в Personконструкторе, поскольку невозможно создать логику с недопустимыми данными - за исключением nullссылок. Конечно, вы должны написать тесты для двух других классов :)
Стивен Бирн
12

Здесь уже хороший ответ, но я думаю, что стоит упомянуть еще одну вещь.

При выполнении TDD «по книге» необходимо сначала написать тест, который вызывает конструктор, даже до того, как конструктор будет реализован. Этот тест на самом деле может выглядеть как тот, который вы представили, даже если бы внутри реализации конструктора была нулевая логика проверки.

Также обратите внимание, что для TDD нужно сначала написать еще один тест, например

  Assert.Throws<ArgumentException>(() => new Person(Guid.NewGuid(), 
        new DateTime(1572, 01, 01));

перед добавлением проверки для DateTime(1900,01,01)конструктора.

В контексте TDD показанный тест имеет смысл.

Док Браун
источник
Хороший угол я не учел!
Лиат
1
Это показывает мне, почему такая жесткая форма TDD является пустой тратой времени: тест должен иметь значение после написания кода, или вы просто пишете каждую строку кода дважды, один раз как утверждение и один раз как код. Я бы сказал, что сам конструктор не является частью логики, которая нуждается в тестировании; бизнес-правило «люди, родившиеся до 1900 года, не должны быть представимыми», является тестируемым, и конструктор - это место, где реализуется это правило, но когда проверка пустого конструктора когда-нибудь добавит ценность проекту?
IMSoP
Это действительно странно по книге? Я бы создал экземпляр и сразу вызвал его метод в коде. Затем я написал бы тест для этого метода, и, сделав это, мне также пришлось бы создать экземпляр для этого метода, чтобы и конструктор, и метод были включены в этот тест. Если в конструкторе нет логики, но эта часть покрыта Лиатом.
Rafał Łuskiyński
@ RafałŁużyński: TDD «по книге» о написании тестов в первую очередь . Это на самом деле означает всегда сначала писать провальный тест (не компилировать также и как провал). Итак, вы сначала пишете тест, вызывающий конструктор, даже когда конструктора нет . Затем вы пытаетесь скомпилировать (что не получается), затем вы реализуете пустой конструктор, компилируете, запускаете тест, result = green. Затем вы пишете первый неудачный тест и запускаете его - result = red, затем добавляете функциональность, чтобы сделать тест снова «зеленым», и так далее.
Док Браун
Конечно. Я не имел в виду, что сначала пишу реализацию, потом тестирую. Я просто пишу «использование» этого кода на уровне выше, затем проверяю этот код, а затем реализую его. Я обычно делаю "Вне ТДД".
Rafał Łużyński