(Чому) важливо, щоб одиничний тест не перевіряв залежності?


103

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

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

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

Яка інша сторона у цьому? Чи справді важливо, щоб одиничний тест також не перевіряв його залежності? Якщо так, то чому?

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


14
"переобладнання, не гарний дизайн". Вам доведеться надати більше доказів, ніж це. Багато людей називали б це не "над інженерією", а "найкращою практикою".
S.Lott

9
@ S.Lott: Звичайно, це суб'єктивно. Я просто не хотів купу відповідей, які викривляють питання і говорять, що глузування добре, тому що створення коду, який можна змінити, сприяє гарному дизайну. Однак, як правило, я ненавиджу справу з кодом, який розв'язується способами, які не мають явної користі зараз або в осяжному майбутньому. Якщо у вас зараз немає декількох реалізацій і не очікуєте їх наявності в осяжному майбутньому, то IMHO вам слід просто жорстко кодувати його. Це простіше і не набридає клієнтові деталями залежності об'єкта.
dimimcha

5
Як правило, я ненавиджу справу з кодом, який поєднаний способами, які не мають очевидного обгрунтування, крім поганого дизайну. Це простіше і не набридає клієнтові гнучкістю перевіряти речі ізольовано.
S.Lott

3
@ S.Lott: Уточнення: я мав на увазі код, який суттєво виходить з шляху його роз'єднання без чіткого випадку використання, особливо там, де він робить код або його клієнтський код значно більш детальним, вводить ще один клас / інтерфейс тощо. Звичайно, я не закликаю код бути більш щільно з'єднаний, ніж найпростіший, найкоротший дизайн. Крім того, коли ви створюєте лінії абстракції передчасно, вони зазвичай закінчуються в неправильних місцях.
dimimcha

Спробуйте оновити своє запитання, щоб уточнити, яку різницю ви робите. Інтегрувати послідовність коментарів складно. Будь ласка, оновіть питання, щоб уточнити та зосередити його.
С.Лотт

Відповіді:


116

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

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


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

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

2
danip: чіткі визначення важливі, і я би наполягав на них. Але також важливо усвідомити, що ці визначення є ціллю. На них варто прагнути, але вам не потрібно завжди бичаче око. Одиничні тести часто матимуть залежність від бібліотек нижчого рівня.
Джеффрі Фауст

2
Також важливо зрозуміти концепцію фасадних тестів, які не перевіряють, як компоненти поєднуються разом (як інтеграційні тести), а протестують один компонент ізольовано. (тобто графік об’єкта)
Рікардо Родрігес

1
справа не тільки в тому, щоб бути швидше, але і в тому, щоб бути надзвичайно виснажливим або некерованим, уявіть, якщо ви цього не зробите, ваше дерево знущань з глузування може зростати експоненціально. Уявіть, що ви знущаєтесь із 10 залежностей у своєму підрозділі, якщо ви продовжуєте глузувати далі, скажімо, 1 з цих 10 залежностей використовується у багатьох місцях, і він має 20 власних залежностей, тож вам доведеться знущатися над + 20 іншими залежностями, потенційно дублюючими у багатьох місцях. в
кінцевому підсумку

39

Тестування з усіма існуючими залежностями все ще важливо, але це більше в царині інтеграційного тестування, як сказав Джеффрі Фауст.

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

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

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

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

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


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

public class MyClass 
{
    private SomeClass someClass;
    public MyClass()
    {
        someClass = new SomeClass();
    }

    // use someClass in some way
}

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

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

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

Перезапис MyClass для прийняття екземпляра SomeClass в конструкторі дозволяє мені створити підроблений екземпляр SomeClass, який повертає потрібне мені значення (або через глузливий фреймворк, або за допомогою макетів вручну). Як правило , в цьому випадку не потрібно вводити інтерфейс. Робити це чи ні, багато в чому це особистий вибір, який може бути продиктований мовою вибору (наприклад, інтерфейси скоріше на C #, але в Ruby вам точно не знадобиться).


+1 Чудова відповідь, адже зовнішні залежності - це випадок, про який я не думав, коли писав оригінальне запитання. Я уточнив своє запитання у відповідь. Дивіться останню редакцію.
dimimcha

@dsimcha Я розширив свою відповідь, щоб детальніше зупинитися на цьому. Сподіваюся, це допомагає.
Адам Лір

Адам, чи можете ви запропонувати мову, де "Якщо SomeClass змінюється і тепер потрібні параметри для конструктора ... я не повинен був би змінювати MyClass"? Вибачте, що просто вискочило на мене як на незвичну вимогу. Я бачу, що не потрібно міняти одиничні тести для MyClass, але не потрібно міняти MyClass ... ух.

1
@moz Що я мав на увазі, що якщо спосіб створення SomeClass, MyClass не повинен змінюватися, якщо SomeClass вводиться в нього, а не створюється внутрішньо. У звичайних обставинах MyClass не повинен дбати про деталі налаштування для SomeClass. Якщо інтерфейс SomeClass змінюється, то так ... MyClass все одно повинен бути змінений, якщо будь-який із методів, які він використовує, впливає.
Адам Лір

1
Якщо ви знущаєтесь з SomeClass під час тестування MyClass, як ви виявите, чи MyClass неправильно використовує SomeClass чи покладається на неперевірену химерність SomeClass, яка може змінитися?
іменний

22

Окрім питання тестування модуля проти інтеграції, врахуйте наступне.

Клас віджет має залежності від класів Thingamajig та WhatsIt.

Тест на віджет не вдався.

У якому класі лежить проблема?

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


3
@Brook: Як щодо того, щоб побачити, які результати є для всіх залежностей віджетів? Якщо всі вони пройдуть, це проблема з Віджетом, поки не доведеться інше.
dimimcha

4
@dsimcha, але тепер ви додаєте складності ще в грі, щоб перевірити проміжні кроки. Чому б не спростити і просто спершу зробити прості тестові пристрої? Потім зробіть свій інтеграційний тест.
asoundmove

1
@dsimcha, це звучить розумно, поки ви не потрапите в графік нетривіальної залежності. Скажімо, у вас складний об’єкт, який має 3+ шари глибиною залежностей, це перетворюється на пошук задачі O (N ^ 2), а не на O (1)
Брук

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

4
@Brook: Більше типів не збільшує складність сама по собі. Більше типів чудово, якщо ці типи мають концептуальний сенс у проблемній області та полегшують розуміння коду, а не складніше. Проблема полягає у штучному роз'єднанні речей, які концептуально поєднані на рівні проблемної домени (тобто у вас є лише одна реалізація, ймовірно, ніколи не буде більше, ніж одна тощо), а розв'язка не відображає добре концепцій проблемних доменів. У цих випадках безглуздо, бюрократично і багатослівно створювати серйозні абстракції навколо цієї реалізації.
dimimcha

14

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

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

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


7
І коли ваші інтеграційні тести не зможуть, тест одиниці може вирішити проблему.
Тім Вілліскрофт

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

Любіть цю аналогію!
thehowler

6

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

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

Обидва важливі. Виконуючи тестування модулів, ви незмінно складніші тонкої помилки, яка виникає при інтеграції компонентів. Просто тестування інтеграції означає, що ви протестуєте систему без впевненості, що окремі частини машини не були протестовані. Думати, що можна досягти кращого повного тесту, просто провівши інтеграційне тестування майже неможливо, оскільки чим більше компонентів, які ти додаєш, кількість комбінацій вступу зростає ДУЖЕ швидко (думаю, факторіальна), і створити тест з достатнім покриттям стає дуже швидко неможливо.

Отже, коротко кажучи, три рівня "одиничного тестування" я зазвичай використовую майже у всіх своїх проектах:

  • Тестові одиниці, виділені з глузливими або заглушеними залежностями, щоб перевірити пекло одного компонента. В ідеалі можна спробувати повністю покрити.
  • Інтеграційні тести для перевірки більш тонких помилок. Ретельно продумані точкові перевірки, які б показували обмежені випадки та типові умови. Повне покриття часто неможливо, зусилля зосереджені на тому, що призведе до виходу з ладу системи.
  • Тестування специфікацій для введення різних даних із реального життя в систему, інструмент даних (якщо це можливо) та спостереження за результатами, щоб забезпечити їх відповідність специфікації та бізнес-правилам.

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


4

Яка інша сторона у цьому? Чи справді важливо, щоб одиничний тест також не перевіряв його залежності? Якщо так, то чому?

Одиниця. Значить однини.

Тестування 2 речей означає, що у вас є дві речі та всі функціональні залежності.

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

Серед n предметів, які випробовуються, є n (n-1) / 2 потенційних залежностей .

Це велика причина.

Простота має значення.


2

Пам'ятаєте, як ви вперше навчилися робити рекурсію? Мій проф сказав "Припустимо, у вас є метод, який робить х" (наприклад, вирішує фібоначки для будь-якого х). "Щоб вирішити для x, ви повинні викликати цей метод для x-1 і x-2". Зважаючи на те, виправлення залежностей дозволяє зробити вигляд, що вони існують, і перевірити, що поточний підрозділ робить те, що повинен робити. Припущення, звичайно, ви випробовуєте залежності як жорстко.

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


2

(Це незначна відповідь. Дякую @TimWilliscroft за підказку.)

Помилки легше локалізувати, якщо:

  • І тестування підрозділу, і кожна його залежність тестуються незалежно.
  • Кожен тест виходить з ладу, якщо і лише в тому випадку, коли в коді, який охоплює тест, є несправність.

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


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

2

Дуже багато хороших відповідей. Я також додам пару інших пунктів:

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

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


0

Щодо дизайнерських аспектів: Я вважаю, що навіть невеликі проекти виграють від того, щоб зробити код перевіряючим. Вам не обов'язково вводити щось на зразок Guice (простий заводський клас часто це робити), але відокремлення процесу побудови від логіки програмування призводить до

  • чітко документувати залежності кожного класу через його інтерфейс (дуже корисно для новачків у команді)
  • класи стають набагато зрозумілішими та простішими в maintan (коли ви покладете створення некрасивого об’єктного графіка в окремий клас)
  • нещільне з'єднання (значно спрощення змін)

2
Метод: Так, але для цього потрібно додати додатковий код та додаткові класи. Код повинен бути максимально стислим, залишаючись читабельним. ІМХО додає фабрики і те, що не є переобладнанням, якщо ви не зможете довести, що вам потрібна або, швидше за все, потрібна гнучкість, яку вона надає.
dimimcha

0

Ага ... в цих відповідях написано хороші моменти щодо тестування модулів та інтеграції!

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

Отже, є й інші фактори, які є набагато важливішими (наприклад, "що тестувати" ), перш ніж ви подумаєте про "як перевірити" з мого досвіду ...

Чи платить мій замовник (неявно) за додаткову суму написання та обслуговування тестів? Чи більш тестовий підхід (спершу пишіть тести, перш ніж писати код) дійсно економічно ефективний у моєму середовищі (аналіз ризику несправності коду / вартість, люди, проектні характеристики, налаштування тестового середовища)? Код завжди баггі, але чи може бути рентабельніше перенести тестування на виробництво (у гіршому випадку!)?

Це також дуже залежить від того, якою є якість вашого коду (стандартів) або рамки, IDE, принципи проектування тощо. Ви та ваша команда дотримуєтесь та досвід роботи. Добре написаний, легко зрозумілий, досить добре документований (в ідеалі самодокументування), модульний, ... код вводить ймовірно менше помилок, ніж навпаки. Тож реальна «потреба», тиск або загальні витрати / технічне обслуговування / помилки для проведення обширних тестів можуть бути не високими.

Давайте розглянемо це до крайності, коли колега з моєї команди запропонував, ми повинні спробувати перевірити наш чистий код шару моделі Java EE з бажаним 100% охопленням для всіх класів в межах та знущається над базою даних. Або менеджер, який хотів би, щоб інтеграційні тести були покриті 100% усіх можливих випадків використання в реальному світі та робочих процесів веб-інтерфейсу, тому що ми не хочемо ризикувати, щоб будь-який випадок використання не відбувся. Але у нас щільний бюджет близько 1 мільйона євро, досить чіткий план, щоб все кодувати. Клієнтське середовище, де потенційні помилки додатків не становлять би великої небезпеки для людей або компаній. Наш додаток буде проходити внутрішнє тестування (деякі) важливі одиничні тести, інтеграційні тести, ключові тести клієнтів із розробленими планами випробувань, тестовою фазою тощо. Ми не розробляємо додаток для ядерної фабрики чи фармацевтичної продукції!

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

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

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

Малюк може і повинен десь піднятися вгору і може впасти і поранити себе. Машина може перестати працювати, оскільки я заповнив неправильне пальне (недійсний вхід :)). Тост може спалитись, якщо кнопка часу перестала працювати через 3 роки. Але я ніколи не хочу їхати по шосе і тримати в руках моє відокремлене кермо :)

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