TDD: Знущання з щільно з’єднаних предметів


10

Іноді об’єкти просто потрібно щільно з'єднати. Наприклад, CsvFileкласу, ймовірно, потрібно буде тісно працювати з CsvRecordкласом (або ICsvRecordінтерфейсом).

Однак, з того, що я дізнався в минулому, одним із головних принципів розробки тесту є "Ніколи не тестуй більше одного класу за один раз". Значить, ви повинні використовувати ICsvRecordмакети або заглушки, а не фактичні екземпляри CsvRecord.

Однак, спробувавши такий підхід, я помітив, що знущання над CsvRecordкласом можуть бути трохи волохатими. Що призводить мене до одного з двох висновків:

  1. Важко писати одиничні тести! Це кодовий запах! Рефактор!
  2. Знущатися над кожною залежною залежністю просто нерозумно.

Коли я замінив свої глузування фактичними CsvRecordекземплярами, все пішло значно гладше. Оглядаючи думки інших людей, я натрапив на цю публікацію в блозі , яка, здається, підтримує №2 вище. Для об’єктів, які природно щільно з'єднані, ми не повинні так сильно хвилюватися над глузуванням.

Чи я не в дорозі? Чи є якісь недоліки до припущення №2 вище? Чи повинен я насправді замислюватися над рефакторингом свого дизайну?


1
Я думаю, що поширене неправильне уявлення про те, що "одиниця" в "одиничних тестах" обов'язково повинна бути одним класом. Я думаю, що ваш приклад показує випадок, коли може бути краще, щоб ці два класи складали одну одиницю. Але не зрозумійте мене неправильно, я повністю згоден з відповіддю Роберта Харві.
Док Браун

Відповіді:


11

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

Однак я заперечую те поняття, яке CsvRecordне є незалежним для перевірки. CsvRecordце в основному клас DTO , чи не так? Це просто колекція полів, можливо з парою допоміжних методів. Крім того, CsvRecordможе бути використаний і в інших контекстах CsvFile; наприклад, ви можете мати колекцію чи масив CsvRecords.

Перевірте CsvRecordспочатку. Переконайтесь, що він пройшов усі свої тести. Потім продовжуйте та користуйтеся CsvRecordзі своїм CsvFileкласом під час тестування. Використовуйте його як попередньо випробуваний заглушок / макет; заповнити його відповідними тестовими даними, передати їх CsvFileта написати свої тестові справи проти цього.


1
Так, CsvRecord, безумовно, незалежно перевіряється. Проблема полягає в тому, що якщо щось порушиться в CsvRecord, це призведе до збою тестів CsvData. Але я не думаю, що це головне питання.
Філ

1
Я думаю, ти хочеш, щоб це сталося. :)
Роберт Харві

1
@RobertHarvey: теоретично це може стати проблемою, якщо CsvRecord і CsvFile стають досить складними класами, і якщо тест перерветься для CsvFile, тепер ви не знаєте негайно, чи це проблема в CsvFile або CsvRecord. Але я думаю, що це більше гіпотетичний випадок - якби я мав завдання програмувати такі класи для програми в реальному світі, я би робив це саме так, як ви це описуєте.
Док Браун

2
@Phil: Якщо CsvRecordламається, то, очевидно, CsvDataне вдається; але це нормально, тому що ви тестуєте CsvRecordспочатку, і якщо це не вдалося, ваші CsvFileтести безглузді. Ви все ще можете розрізняти помилки в CsvRecordі в CsvFile.
tdammers

5

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

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

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

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


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

1

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


0

"З'єднані" класи взаємно залежать один від одного. Це не повинно бути в тому, що ви описуєте - CsvRecord не повинен насправді дбати про CsvFile, що його містить, тому залежність йде лише в один бік. Це добре, і це не тісна муфта.

Зрештою, якщо клас містить ім'я змінної String, ви б не стверджували, що він щільно пов'язаний зі String, чи не так?

Отже, блок тестує CsvRecord на бажану поведінку.

Потім використовуйте глузуючий фреймворк (Mockito чудово), щоб перевірити, чи ваш пристрій взаємодіє з об'єктами, від яких правильно залежить. Поведінка, яку ви хочете перевірити, насправді - це CsvFile обробляє CsvRcords очікуваним чином. Внутрішня робота CvsRecord не має значення - це те, як CvsFile спілкується з нею.

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


1
-1, щільне з'єднання не обов'язково означає циклічну залежність, це неправильне уявлення. У прикладі, CsvFile як тісно пов'язані з CsvRecord(але не навпаки). ОП запитує, чи є хороша ідея тестування CsvFile, від’єднавши її від CsvRecordчерез ICsvRecord, а не навпаки.
Док Браун

2
@DocBrown: Незалежно від того, чи є з'єднання герметичним, чи ні, це залежить від того, наскільки CsvFileзалежить внутрішня робота CsvRecord, тобто кількість припущень, які має файл щодо запису. Інтерфейси допомагають документувати та виконувати такі припущення (точніше, відсутність інших припущень), але кількість з'єднань залишається однаковою, за винятком інтерфейсу, до якого можна підключити інший клас записів CsvFile. Представляти інтерфейс просто так, що можна сказати, що у вас зменшена муфта - нерозумно.
tdammers

0

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

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

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

Більшість людей, здається, припускають, що ваш CsvRecordклас цінності. Спробуйте зробити його одним. Зробіть це незмінним, якщо можете. Якщо у вас є два об’єкти з покажчиками один на одного, видаліть один з них і з’ясуйте, як змусити його працювати. Шукайте місця для поділу класів та функцій. Найкраще місце для розділення класу може не завжди збігатися з фізичним розташуванням файлу. Спробуйте змінити відносини між батьками та дітьми класів. Можливо, вам потрібен окремий клас для читання та запису файлів CSV. Можливо, вам потрібні окремі класи для обробки вводу / виводу файлів та інтерфейсу до верхніх шарів. Є багато чого, що слід спробувати, перш ніж визнати це незмінним.

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