Тестування підрозділу класу, який використовує DI без тестування на внутрішніх


12

У мене є клас, який реконструюється в 1 основному класі та в 2 менших класах. Основні класи використовують базу даних (як це робить багато моїх класів) і надсилають електронний лист. Таким чином, у основного класу є IPersonRepositoryі IEmailRepositoryвводиться ін'єкція, яка в свою чергу надсилає до 2-х менших класів.

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

Але , як клас використовує IPersonRepositoryі IEmailRepositoryя МАЮ вказати (макет / манекен) результати для деяких методів для IPersonRepository. Основний клас обчислює деякі дані на основі наявних даних і повертає їх. Якщо я хочу це перевірити, я не бачу, як я можу написати тест, не вказуючи, що IPersonRepository.GetSavingsByCustomerIdreturn x. Але тоді мій блок-тест "знає" про внутрішні розробки, тому що він "знає", які методи знущатися, а які ні.

Як я можу перевірити клас, у якого введені залежності, не знаючи тесту про внутрішню?

фон:

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

Тепер я бачив презентацію про теорію (яку я чув раніше), що тест не повинен знати про реалізацію. Спочатку тому, що ви не тестуєте, як це працює, а також тому, що, коли ви зараз змінюєте реалізацію, всі тести блоку виходять з ладу, оскільки вони "знають" про реалізацію. Хоча мені подобається, що концепція тестів не знає про реалізацію, я не знаю, як її виконати.


1
Як тільки ваш основний клас очікує IPersonRepositoryоб'єкт, цей інтерфейс і всі описані ним методи вже не є "внутрішніми", тому це насправді не є проблемою тесту. Ваше справжнє запитання повинно полягати в тому, "як я можу переробляти класи на менші одиниці, не піддаючись занадто сильному публічному". Відповідь - "тримати ці інтерфейси нахиленими" (наприклад, дотримуючись принципу сегментації інтерфейсу). Це пункт 2 ІМХО у відповіді @ DavidArno (я думаю, немає потреби повторювати це в іншій відповіді).
Док Браун

Відповіді:


7

Основний клас обчислює деякі дані на основі наявних даних і повертає їх. Якщо я хочу це перевірити, я не бачу, як я можу написати тест, не вказуючи, що IPersonRepository.GetSavingsByCustomerIdreturn x. Але тоді мій блок-тест "знає" про внутрішні розробки, тому що він "знає", які методи знущатися, а які ні.

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

Як я можу перевірити клас, у якого введені залежності, не знаючи тесту про внутрішню?

Ви можете прийняти два рішення, щоб вирішити це порушення:

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

Цей підхід має перевагу в простоті механізму DI, але він може створити багато роботи, якщо ваш інтерфейс визначає багато методів.

2) Не вводити IPersonRepositoryабо вводити GetSavingsByCustomerIdметод, або не вводити значення економії. Проблема з ін'єкцією цілих реалізацій інтерфейсу полягає в тому, що ви ін'єктуєте ("скажіть, не запитуйте") систему "запитати, не кажи", змішуючи два підходи. Якщо ви використовуєте "чистий DI" підхід, метод повинен передбачити (розповісти) точний метод для виклику, якщо він хоче значення економії, а не присвоєння об'єкту (який він повинен ефективно вимагати, щоб метод викликав).

Перевага такого підходу полягає в тому, що він уникає необхідності знущань (крім методів тестування, які ви вводите в тестований метод). Недоліком є ​​те, що він може спричинити зміну підписів методів у відповідь на зміни вимог методу.

Обидва підходи мають свої плюси і мінуси, тому виберіть той, який найкраще відповідає вашим потребам.


1
Мені дуже подобається ідея № 2. Я не знаю, чому раніше про це не думав. Я створюю залежності від IRepositories вже досить багато років, не замислюючись над тим, чому я створив залежність від цілого IRepository (який у багатьох випадках міститиме досить багато підписів методів), хоча він має залежність лише від одного або 2 методи. Тому замість введення IRepository я міг би краще ввести або пройти вздовж необхідного лише підпису методу.
Мішель

1

Мій підхід полягає у створенні "макетних" версій сховищ, які читаються з простих файлів, що містять необхідні дані.

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

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

Оскільки «макет» повністю реалізований, а не спеціально налаштований на ваш тестовий сценарій, зміна реалізації, наприклад, дозволяє сказати, що GetSavingsForCustomer повинен також видалити клієнта. Не вийде з ладу тестів (якщо, звичайно, це справді не порушує тести), вам потрібно лише оновити вашу єдину макетну реалізацію, і всі ваші тести будуть працювати проти неї, не змінюючи їх налаштування


2
Це все ще призводить до того, що я створюю макет з даними саме для методів, з якими працює тестований клас. Тож у мене все ще виникає питання про те, що коли зміниться реалізація, тест зламається.
Мішель

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

1

Тестові одиниці, як правило, є тестами білого поля (у вас є доступ до реального коду). Тому нормально знати внутрішню інформацію до певної міри, однак для початківців легше це не робити, тому що не слід перевіряти внутрішню поведінку (наприклад, "виклик методу спочатку, потім b, а потім знову").

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.