Тестування приладів у світі "без сетера"


23

Я не вважаю себе експертом DDD, але, як архітектор рішення, намагаюся застосовувати кращі практики, коли це можливо. Я знаю, що існує багато дискусій навколо плюсів і мінусів не (публічного) сеттерського "стилю" в DDD, і я можу побачити обидві сторони аргументу. Моя проблема полягає в тому, що я працюю в команді з широким розмаїттям вмінь, знань та досвіду, що означає, що я не можу вірити, що кожен розробник буде робити справи "правильним" шляхом. Наприклад, якщо наші об’єкти домену розроблені таким чином, що зміни у внутрішньому стані об'єкта здійснюються методом, але надають налаштуванням публічної власності, хтось неминуче встановить властивість замість виклику методу. Скористайтеся цим прикладом:

public class MyClass
{
    public Boolean IsPublished
    {
        get { return PublishDate != null; }
    }

    public DateTime? PublishDate { get; set; }

    public void Publish()
    {
        if (IsPublished)
            throw new InvalidOperationException("Already published.");

        PublishDate = DateTime.Today;

        Raise(new PublishedEvent());
    }
}

Моє рішення полягало в тому, щоб зробити налаштування властивостей приватними, що можливо, тому що ORM, який ми використовуємо для гідратації об'єктів, використовує відображення, щоб він мав доступ до приватних установників. Однак це створює проблему при спробі написання одиничних тестів. Наприклад, коли я хочу написати одиничний тест, який підтверджує вимогу, що ми не можемо повторно опублікувати, мені потрібно зазначити, що об’єкт уже опублікований. Я, безумовно, можу це зробити, зателефонувавши Publish двічі, але тоді мій тест передбачає, що Publish реалізований правильно для першого дзвінка. Це здається трохи смердючим.

Давайте зробимо сценарій трохи більш реальним із наступним кодом:

public class Document
{
    public Document(String title)
    {
        if (String.IsNullOrWhiteSpace(title))
            throw new ArgumentException("title");

        Title = title;
    }

    public String ApprovedBy { get; private set; }
    public DateTime? ApprovedOn { get; private set; }
    public Boolean IsApproved { get; private set; }
    public Boolean IsPublished { get; private set; }
    public String PublishedBy { get; private set; }
    public DateTime? PublishedOn { get; private set; }
    public String Title { get; private set; }

    public void Approve(String by)
    {
        if (IsApproved)
            throw new InvalidOperationException("Already approved.");

        ApprovedBy = by;
        ApprovedOn = DateTime.Today;
        IsApproved = true;

        Raise(new ApprovedEvent(Title));
    }

    public void Publish(String by)
    {
        if (IsPublished)
            throw new InvalidOperationException("Already published.");

        if (!IsApproved)
            throw new InvalidOperationException("Cannot publish until approved.");

        PublishedBy = by;
        PublishedOn = DateTime.Today;
        IsPublished = true;

        Raise(new PublishedEvent(Title));
    }
}

Я хочу написати одиничні тести, які підтверджують:

  • Я не можу публікувати, якщо документ не затверджено
  • Я не можу повторно опублікувати документ
  • Після публікації значення Pubqy та PublishedOn правильно встановлені
  • Коли публікується, PubEvent піднімається

Без доступу до сетерів я не можу поставити об'єкт у стан, необхідний для проведення тестів. Відкриття доступу до сетерів перемагає мету запобігання доступу.

Як ви (вирішили) вирішити цю проблему?


Чим більше я думаю про це, тим більше я вважаю, що вся ваша проблема полягає у використанні методів із побічними ефектами. А точніше - незмінний незмінний предмет. Чи не слід у світі DDD повертати новий об'єкт Document із програми «Затвердити» та «Опублікувати», а не оновлювати внутрішній стан цього об’єкта?
pdr

1
Швидке запитання, який O / RM ви використовуєте. Я великий фанат EF, але оголошення сетерів захищеними трохи не втирає мене.
Майкл Браун

Зараз у нас є мікс через розробку вільного діапазону, яку мені доручили втручатись. Деякі ADO.NET, що використовують AutoMapper, для гідратації з DataReader, декількох моделей Linq-SQL (це буде наступною заміною ) та деякі нові моделі EF.
SonOfPirate

Виклик публікації двічі зовсім не смердючий, і це спосіб зробити це.
Пьотр Перак

Відповіді:


27

Я не можу поставити об'єкт у стан, необхідний для проведення тестів.

Якщо ви не можете помістити об'єкт у стан, необхідний для проведення тесту, ви не можете перевести об'єкт у стан у виробничому коді, тому не потрібно тестувати цей стан. Очевидно, що це не так у вашому випадку, ви можете перевести свій об'єкт у потрібний стан, просто зателефонуйте Підтвердити.

  • Я не можу публікувати, якщо документ не затверджено: написати тест, який викликає публікацію перед затвердженням виклику, викликає правильну помилку без зміни стану об'єкта.

    void testPublishBeforeApprove() {
        doc = new Document("Doc");
        AssertRaises(doc.publish, ..., NotApprovedException);
    }
    
  • Я не можу повторно опублікувати документ: написати тест, який затверджує об'єкт, потім викликати публікацію, коли це вдасться, але вдруге викликає правильну помилку, не змінюючи стан об'єкта.

    void testRePublish() {
        doc = new Document("Doc");
        doc.approve();
        doc.publish();
        AssertRaises(doc.publish, ..., RepublishException);
    }
    
  • Коли публікуються, значення PublishedBy та PublishedOn правильно встановлені: написати тест, який викликає схвалення, потім виклик публікувати, і стверджувати, що стан об'єкта змінюється правильно

    void testPublish() {
        doc = new Document("Doc");
        doc.approve();
        doc.publish();
        Assert(doc.PublishedBy, ...);
        ...
    }
    
  • Коли публікується, PublishedEvent піднімається: підключіть до системи подій і встановіть прапор, щоб переконатися, що він викликається

Вам також потрібно написати тест для затвердження.

Іншим словом, не перевіряйте співвідношення між внутрішніми полями та IsPublished та IsApproved, ваш тест був би досить крихким, якщо ви це зробите, оскільки зміна поля означатиме зміну коду тесту, тому тест був би зовсім безглуздим. Натомість слід перевірити зв’язок між викликами публічних методів таким чином, навіть якщо ви змінюєте поля, вам не потрібно буде змінювати тест.


Коли затвердити перерви, кілька тестів перерваються. Ви більше не тестуєте одиницю коду, ви протестуєте повну реалізацію.
пдр

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

4
Я ще не бачив одиничного тесту, який міг би провалитися лише з єдиної можливої ​​причини. Крім того, ви можете помістити частину тесту "маніпулювання станом" у setup()метод --- а не сам тест.
Пітер К.

12
Чому залежність від approve()якось крихкої, але залежно від setApproved(true)якось ні? approve()є законною залежністю у тестах, оскільки це залежність від вимог. Якби залежність існувала лише в тестах, це було б іншим питанням.
Карл Білефельдт

2
@pdr, як би ви протестували клас стека? Ви б спробували випробувати push()і pop()методи самостійно?
Вінстон Еверт

2

Ще один підхід полягає у створенні конструктора класу, який дозволяє встановлювати внутрішні властивості при інстанціюванні:

 public Document(
  String approvedBy,
  DateTime? approvedOn,
  Boolean isApproved,
  Boolean isPublished,
  String publishedBy,
  DateTime? publishedOn,
  String title)
{
  ApprovedBy = approvedBy;
  ApprovedOn = approvedOn;
  IsApproved = isApproved;
  IsApproved = isApproved;
  PublishedBy = publishedBy;
  PublishedOn = publishedOn;
}

2
Це зовсім не добре масштабується. Мій об’єкт може мати ще багато властивостей, будь-яка їх кількість має або не має значення в будь-якій точці життєвого циклу об'єкта. Я дотримуюся принципу, що конструктори містять параметри для властивостей, необхідних для того, щоб об'єкт знаходився у дійсному початковому стані або залежностях, необхідних для функціонування об'єкта. Метою властивостей у прикладі є захоплення поточного стану під час маніпулювання об'єктом. Мати конструктор з усіма властивостями або перевантажувати різні комбінації - це величезний запах і, як я вже сказав, не масштабується.
SonOfPirate

Зрозумів. У вашому прикладі не було згадано ще багато властивостей, і число у прикладі "на самом деле", щоб мати це як правильний підхід. Здається, що це щось говорить про ваш дизайн: ви не можете поставити об'єкт у будь-який дійсний стан при встановленні. Це означає, що вам потрібно перевести його у дійсний початковий стан, і вони маніпулюють ним у правильному стані для перевірки. Це означає , що відповідь Лі Раяна - це шлях .
Петро К.

Навіть якщо об’єкт має одну властивість і ніколи не змінить це рішення, це погано. Що заважає комусь використовувати цей конструктор у виробництві? Як ви позначите цей конструктор [TestOnly]?
Пьотр Перак

Чому це погано у виробництві? (Дійсно, я хотів би це знати). Іноді необхідно відтворити точний стан об'єкта при створенні ... не просто одного дійсного початкового об'єкта.
Пітер К.

1
Тож, хоча це допомагає привести об'єкт у дійсний початковий стан, тестування поведінки об'єкта на ньому прогресує через його життєвий цикл, вимагає змінити об'єкт із початкового стану. Мій ОП пов'язаний з тестуванням цих додаткових станів, коли ви не можете просто встановити властивості для зміни стану об'єкта.
SonOfPirate

1

Одна з стратегій полягає в тому, що ви успадковуєте клас (у цьому випадку Document) і пишете тести проти спадкового класу. Спадковий клас дозволяє певним чином встановити стан об'єкта в тестах.

У C # one стратегія може полягати в тому, щоб зробити сетерів внутрішніми, а потім викрити внутрішні тести для проекту.

Ви також можете використовувати API класу, як ви описали ("Я, безумовно, можу це зробити, зателефонувавши Опублікувати двічі"). Це було б встановлення стану об'єкта за допомогою публічних служб об'єкта, мені це не здається занадто смердючим. У випадку вашого прикладу, мабуть, це було б так, як я це зробив.


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

1

Щоб перевірити в абсолютній ізоляції команди та запити, які отримують об’єкти домену, я використовую для забезпечення кожного тесту серіалізацією об'єкта в очікуваному стані. У розділі "Упорядкувати" тесту він завантажує об'єкт для тестування з файлу, який я раніше підготував. Спочатку я почав з бінарних серіалізацій, але json виявився набагато простішим для утримання. Це виявилося добре, коли абсолютна ізоляція в тестах забезпечує фактичну цінність.

відредагуйте лише нотатку, інколи серіалізація JSON не вдається (як у випадку графіків циклічного об'єкта, які є запахом, btw). У таких ситуаціях я рятуюся до бінарної серіалізації. Це трохи прагматично, але працює. :-)


І як ви готуєте об'єкт у очікуваному стані, якщо немає сеттера і ви не хочете називати його публічними методами для його налаштування?
Пьотр Перак

Я написав для цього невеликий інструмент. Він завантажує клас за допомогою відображення, який створює нову сутність за допомогою свого загальнодоступного конструктора (зазвичай бере лише ідентифікатор) і викликає на нього масив Action <TEntity>, зберігаючи знімок після кожної операції (зі звичайною назвою на основі індексу дії і ім'я сутності). Інструмент виконується вручну під час кожного рефакторингу коду сутності, а знімки відстежуються DCVS. Очевидно, що кожна дія викликає публічну команду суб'єкта, але це робиться поза тестами, що таким чином є справді тестовим модулем .
Джакомо Тесіо

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

Після створення знімків вони зберігаються у файлах. Кожен тест залежить не від послідовності операцій, необхідних для отримання початкового стану сутності, а від самого стану (завантаженого із знімка). Сам тестований метод потім ізолюється від змін до інших методів.
Джакомо Тесіо

Що робити, коли хтось змінює публічний метод, який використовувався для підготовки серіалізованого стану до ваших тестів, але забуває запустити інструмент для відновлення серіалізованого об'єкта? Тести все ще зелені, навіть якщо в коді є помилка. Я все ж кажу, що це нічого не змінює. Ви все ще використовуєте загальнодоступні методи, щоб налаштувати тестовані об’єкти. Але ви запускаєте їх задовго до запуску тестів.
Пьотр Перак

-7

Ти кажеш

намагайтеся застосовувати кращі практики, коли це можливо

і

ORM, який ми використовуємо для гідратації об'єктів, використовує відображення, щоб він мав доступ до приватних сеттерів

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


Особисто я б зламав ваш блок тестування одиниць і пішов із чим-небудь у класі - схоже, ви пишете тести з точки зору тестування всього класу, що добре. Раніше, для деяких складних компонентів, які потребували тестування, я вкладав твердження та код налаштування в сам клас (це був звичайний шаблон дизайну, щоб мати метод test () у кожному класі), тому ви створюєте клієнта що просто створює об'єкт і викликає метод тестування, який може налаштувати себе так, як вам подобається, без гнучкості, як рефлекторні хаки.

Якщо вас турбує розмиття коду, просто загорніть методи тестування в #ifdefs, щоб зробити їх доступними лише в коді налагодження (можливо, найкраща практика)


4
-1: Скасування рамки тестування та повернення до методів тестування всередині класу повертаються до темних віків одиничного тестування.
Роберт Джонсон

9
Ні -1 від мене, але включати тестовий код у виробництво, як правило, погана річ (TM) .
Петро К.

що ще робить ОП? Дотримуйтесь прикручування з приватними сетерами ?! Це як вибирати, яку отруту ви хочете пити. Моя пропозиція до ОП полягала в тому, щоб тест одиниці був внесений у код налагодження, а не виробництво. На моєму досвіді, тестування одиничних тестів в інший проект просто означає, що проект так чи інакше тісно пов'язаний з оригіналом, тому від розробленого PoV розрізнення мало.
gbjbaanb
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.