Тест блоку для перевірки створення об’єкта домену


11

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

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

Я стверджую, що тут створюється об'єкт Person, тобто перевірка не проходить. Наприклад, якщо Посібник є недійсним або дата народження є раніше 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;
    }

Це хороша ідея для тесту?

Примітка . Я дотримуюсь класицистичного підходу до тестування одиничної моделі домену, якщо це має якесь значення.


Чи має конструктор якась логіка, яку варто стверджувати після ініціалізації?
Лаїв

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;
}

Тоді ваш тест стає більш релевантним (оскільки ви насправді кидаєте винятки десь у коді).

Я б сказав одне, загалом це погана практика - мати багато логіки у вашому конструкторі. Базова перевірка (як, наприклад, перевірка на нуль / за замовчуванням, яку я роблю вище) нормальна. Але якщо ви підключаєтесь до баз даних і завантажуєте чиїсь дані, то тут код починає насправді пахнути ...

Через це, якщо ваш конструктор вартий тестування (тому що логічно багато), можливо, щось інше не так.

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

У вашому прикладі:

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

Ці тести мають певне значення, однак логіка перевірки / призначення, ймовірно, буде охоплена іншими тестами у вашому рішенні. Якщо в цих конструкторах багато логіки, яка вимагає значного тестування, то мені це підказує, що там криється неприємний запах коду.


@Laith, дивіться оновлення до мого питання
w0051977

Я помічаю, що ви називаєте конструктор бази у своєму прикладі. IMHO це додає більшої цінності вашому тесту, логіка конструктора тепер розділена на два класи і, отже, трохи більший ризик змін, тому даючи більше підстав для тестування.
Ліат

"Однак, якщо ваш тест робить щось подібне:" <Чи не ви маєте на увазі "якщо ваш конструктор робить щось подібне" ?
Кодос Джонсон

"Існує деяка цінність для цих тестів" - для мене все одно, що це значення показує, що ми могли б зробити цей тест надмірним, використовуючи новий клас для представлення доходу людини (наприклад 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

Це справді tdd від книги? Я б створив екземпляр і назвав його метод відразу в коді. Тоді я б написав тест для цього методу, і, зробивши це, я також повинен створити екземпляр для цього методу, тому і конструктор, і метод будуть охоплені цим тестом. Якщо в конструкторі немає певної логіки, але ця частина прикрита Ліатом.
Rafał Łużyński

@ RafałŁużyński: TDD "за книгою" - це спочатку про тестування тестів . Це фактично означає, що завжди спочатку писати провальний тест (не складати також рахунки як збій). Отже, ви спочатку пишете тест, який викликає конструктор, навіть коли конструктора немає . Потім ви намагаєтеся компілювати (що не вдається), потім реалізуєте порожній конструктор, компілюєте, запускаєте тест, результат = зелений. Потім ви пишете перший невдалий тест і запускаєте його - результат = червоний, потім додаєте функціональність, щоб тест знову став «зеленим» тощо.
Док Браун

Звичайно. Я не мав на увазі, що спершу пишу реалізацію, а потім тест. Я просто записую "використання" цього коду на рівні вище, потім тестую цей код, а потім його реалізую. Я зазвичай роблю "Зовнішнє TDD".
Rafał Łużyński
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.