Чи є шпигунство на перевіреному класі поганою практикою?


14

Я працюю над проектом, де внутрішні дзвінки класу звичайні, але результати багато разів прості. Приклад ( не реальний код ):

public boolean findError(Set<Thing1> set1, Set<Thing2> set2) {
  if (!checkFirstCondition(set1, set2)) {
    return false;
  }
  if (!checkSecondCondition(set1, set2)) {
    return false;
  }
  return true;
}

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

Робоче рішення; однак, це зробити тестований об'єкт шпигуном та знущатися над дзвінками до внутрішніх функцій.

systemUnderTest = Mockito.spy(systemUnderTest);
doReturn(true).when(systemUnderTest).checkFirstCondition(....);

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

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

Відповіді:


15

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

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


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

@allprog Коли вам потрібно багато глузувати, здається, що у вас занадто багато залежностей між класами. Ви намагалися зменшити зв’язок між ними?
Філіпп

@allprog, якщо ви опинилися в цій ситуації, винна конструкція класу.
йогобрюс

Саме модель даних викликає головний біль. Він повинен дотримуватися правил ORM та багатьох інших вимог. З чистою бізнес-логікою та кодом без громадянства набагато простіше правильно підібрати одиничні тести.
allprog

3
Тести блоку не обов'язково повинні обробляти SUT як резервну скриньку. Саме тому їх називають одиничними тестами. Знущаючись над залежностями, я можу впливати на навколишнє середовище і знати, з чого мені знущатися, я повинен знати і деякі внутрішні. Але це, звичайно, не означає, що SUT слід будь-яким чином змінювати. Шпигунство, однак, дозволяє внести деякі зміни.
allprog

4

Якщо findError()і checkFirstCondition()т. Д. Є загальнодоступними методами вашого класу, тоді findError()фактично є фасад для функціональності, який уже доступний в тому ж API. У цьому немає нічого поганого, але це означає, що ви повинні написати для нього тести, які дуже схожі на вже існуючі тести. Це дублювання просто відображає дублювання у вашому загальнодоступному інтерфейсі. Це не є підставою для того, щоб цей спосіб по-різному ставитися до інших.


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

3
Я не погоджуюся з тим, що для належного тестування підрозділи допоміжні методи повинні бути загальнодоступними. Якщо в договорі методу зазначено, що він перевіряє на різні умови, то немає нічого поганого в тому, щоб написати кілька тестів проти одного публічного методу, по одному для кожного «субпідряду». Суть одиничних тестів полягає у досягненні охоплення всього коду, а не у досягненні поверхневого покриття публічними методами за допомогою відповідності тесту 1: 1.
Кіліан Фот

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

4

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

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


Гарно звучить. Але насправді "рядок", який я маю вводити і називати його кодом, є мовою, яка дуже мало знає про функції. Теоретично я можу легко описати проблему і зробити заміни тут і там, щоб спростити її. У код я повинен додати багато синтаксичного шуму, щоб досягти цієї гнучкості, яка відхиляє мене від його використання. Якщо метод aмістить виклик методу bтого ж класу, то тести aповинні включати тести b. І немає способу змінити це до тих пір, bпоки не буде передано aпараметр, але я не бачу іншого рішення.
allprog

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

Дивіться мій коментар до відповіді @ Філіпа. Я ще не згадував, але модель даних є джерелом зла. Чистий код без громадянства - це шматок пирога.
allprog

2

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

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

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


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

З початком функціонального програмування в Java 8 це стало трохи більш елегантним, але все-таки збереження функціональності в одному класі може бути кращим вибором у випадку алгоритмів, а не вилучення різних (тільки не корисних) частин для "використання один раз" заняття саме через заповітність. У цьому відношенні шпигуни роблять те саме, що знущаються, але без необхідності візуально підірвати когерентний код. Насправді використовується той же код налаштування, що і для макетів. Мені подобається триматися подалі від крайнощів, кожен тип тесту може бути відповідним у певних місцях. Тестувати якось набагато краще, ніж зараз. :)
allprog

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

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

@allprog "логічно складний" - Якщо його складний, вам потрібні складні тести. Навколо цього немає способу. Шпигуни просто збираються зробити це складніше і складніше для вас. Вам слід створювати зрозумілі підфункції, які ви можете перевірити самостійно, а не використовувати шпигунів для перевірки їх особливої ​​поведінки всередині іншої функції.
BT
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.