Чи TDD робить захисне програмування зайвим?


104

Сьогодні у мене була цікава дискусія з колегою.

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

У конкретній ситуації, в якій я сьогодні обговорював, я написав код, який підтверджує, що аргументи мого конструктора є правильними (наприклад, цілий параметр повинен бути> 0), і якщо попередня умова не виконується, викидається виняток. Мій колега, з іншого боку, вважає, що така перевірка є зайвою, тому що одиничні тести повинні сприймати будь-які неправильні способи використання класу. Крім того, він вважає, що перевірки оборонного програмування також повинні бути перевірені одиницею, тому оборонне програмування додає багато роботи і тому не є оптимальним для TDD.

Чи правда, що TDD здатна замінити оборонне програмування? Чи валідація параметрів (і я не маю на увазі користувальницькі дані) є наслідком непотрібною? Або дві техніки доповнюють одна одну?


120
Ви передаєте свою повністю перевірену підрозділом бібліотеку без перевірок конструктора клієнтові на використання, і вони порушують контракт класу. Яку користь отримують ті одиничні тести зараз?
Роберт Харві

42
ІМО - це навпаки. Захисне програмування, належні попередні та умовні умови та система багатого типу роблять тести зайвими.
садок

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

16
"одиничні тести повинні вловлювати будь-які неправильні способи використання класу" - е-е, як? Блок тестів покаже вам поведінку за правильними аргументами та при неправильних аргументах; вони не можуть показати вам усі аргументи, які вони коли-небудь будуть наводити.
OJFord

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

Відповіді:


196

Це смішно. TDD змушує код проходити тести і змушує весь код мати кілька тестів навколо нього. Це не заважає вашим споживачам неправильно викликати код, а також не заважає програмістам пропускати тестові випадки.

Жодна методологія не може змусити користувачів правильно використовувати код.

Там є невеликий аргумент , який буде зроблений , що якщо ви відмінно зробили TDD ви б зловили чек> 0 в тестовому випадку, до його реалізації, і це ім'я - ймовірно, ви додавши чек. Але якщо ви зробили TDD, ваша вимога (> 0 в конструкторі) спочатку з'явиться як тестовий зразок, який не вдається. Таким чином, ви отримаєте тест після додавання чека.

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

Або дві техніки доповнюють одна одну?

TDD розробить тести. Здійснення перевірки параметрів змусить їх пройти.


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

20
@ user2180613 Створіть тест, який перевіряє, що помилка попередньої умови обробляється належним чином: тепер додавання чека не є "зайвою" роботою, це робота, необхідна TDD, щоб тест став зеленим. Якщо думка вашого колеги полягає в тому, що ви повинні зробити тест, спостерігати за його відмовою, а потім і лише потім здійснити перевірку передумов, то він може мати крапку з точки зору TDD-пуриста. Якщо він говорить просто ігнорувати чек повністю, то він дурний. У TDD немає нічого, що говорить про те, що ви не можете проявляти активність у написанні тестів на потенційні режими відмови.
RM

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

@ user2180613 Ось якесь дивовижне виправдання: D якщо ваша мета написання програмного забезпечення - ви зменшите кількість тестів, які вам потрібно авторувати та запускати, не пишіть жодного програмного забезпечення - нульові тести!
Гусдор

3
Це останнє речення цієї відповіді нівелює це.
Роберт Грант

32

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

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

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


Чи правильно сказати, що валідація параметрів є формою перевірки передумов, а одиничні тести - це підтвердження після умови, тому вони доповнюють одна одну?
користувач2180613

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

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

1
@ jpmc26 Так, невдача - це "очікуваний результат" для тесту. Ви перевіряєте, щоб показати, що це не вдається, а не мовчки демонструвати якусь невизначену (несподівану) поведінку.
KRyan

6
TDD виявляє помилки у власному коді, захисне програмування виявляє помилки в коді інших людей. TDD таким чином може допомогти вам забезпечити достатню оборону :)
jwenting

30

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

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

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

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

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


16

Тести є для підтримки та забезпечення оборонного програмування

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

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

EDIT: Аналогія

Як щодо аналогії з коментарями в коді?

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

Тому скажіть, що ви поставили багато тестових знань про свою кодову базу в тести, такі як MethodA не може взяти нуль, а аргумент MethodB повинен бути > 0. Потім код змінюється. Нуль нормально для A зараз, і B може приймати значення як -10. Існуючі тести тепер функціонально помиляються, але продовжуватимуть проходити.

Так, вам слід оновлювати тести одночасно з оновленням коду. Ви також повинні оновлювати (або видаляти) коментарі одночасно з оновленням коду. Але всі ми знаємо, що це не завжди буває, і це помилки.

Тести перевіряють поведінку системи. Ця реальна поведінка властива самій системі, а не властива тестам.

Що може піти не так?

Мета тестів - придумати все, що може піти не так, написати тест на нього, який перевіряє правильність поведінки, а потім скласти код виконання, щоб він пройшов усі тести.

Що означає, що оборонне програмування - це суть .

TDD управляє оборонним програмуванням, якщо випробування є комплексними.

Більше тестів, водіння більш захисного програмування

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

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

Загалом кажучи ...

Наприклад, якщо аргумент нуля для певної процедури недійсний, то принаймні один тест пройде нуль, і він очікує винятку / помилки "недійсний нульовий аргумент".

Принаймні один інший тест, звичайно, пройде дійсний аргумент - або прокрутить великий масив і передасть незрозумілі вірні аргументи - і підтвердить, що отриманий стан є відповідним.

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

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

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

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


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

1
:-) Я просто проголосував, не читаючи більше першого абзацу, тому, сподіваюся, це збалансує це ...
SusanW

1
Здалося що я міг зробити :-) ( На самом деле, я навіть читати інші просто щоб переконатися , що не повинні бути неакуратним -.! Особливо на тему , як це)
SusanW

1
Я порахував, що ти, мабуть, мав. :)
Крейг

захисні перевірки можна проводити під час компіляції з такими інструментами, як Код контрактів.
Матвій Віт

9

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


Отже, оскільки ми робимо тестування програмного забезпечення, нам слід кинути розміщення тверджень про твердження у виробничому коді, правда? Дозвольте порахувати, як це неправильно:

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

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

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

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

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

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

  7. Твердження зменшують складність програми. Кожен рядок коду, який ви пишете, збільшує складність програми. Твердження і ключове слово final( readonly) є єдиними двома конструкціями, про які я знаю, що фактично знижують складність програми. Це безцінно.

  8. Твердження допомагають компілятору краще зрозуміти ваш код. Будь ласка, спробуйте це в домашніх умовах: void foo( Object x ) { assert x != null; if( x == null ) { } }ваш компілятор повинен видати попередження про те, що умова x == nullзавжди помилкова. Це може бути дуже корисно.

Вище було підсумком публікації з мого блогу, 2014-09-21 "Затвердження та тестування"


Я думаю, що я з більшою мірою не згоден з цією відповіддю. (5) У TDD тестовим набором є специфікація. Ви повинні написати найпростіший код, щоб зробити тести проходять, нічого більше. (4) Червоно-зелений робочий процес гарантує, що тест закінчується, коли він повинен проходити, і проходить, коли передбачена функціональність. Твердження тут не дуже допомагають. (3,7) Документація - це документація, твердження - ні. Але зробивши припущення явними, код стає більш самодокументованим. Я б вважав їх виконуваними коментарями. (2) Тестування в білій коробці може бути частиною дійсної тестової стратегії.
амон

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

5

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

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

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

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


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

@ Петерис: Ви думаєте про невизначену поведінку, як у С? Викликати невизначене поведінку, яке має різний результат у різних середовищах, очевидно, є помилкою, але її також не можна запобігти за допомогою перевірки попередніх умов. Наприклад, як перевірити точки вказівки аргументу на дійсну пам'ять?
ЖакБ

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

1
@RobertHarvey: У такому випадку система повинна бути розділена на підсистеми з чітко визначеними інтерфейсами та перевірку вхідних даних, що виконуються на інтерфейсі.
ЖакБ

це. Це залежить від коду, чи повинен цей код використовувати команда? Чи має команда доступ до вихідного коду? Якщо його суто внутрішній код, то перевірка аргументів може бути просто тягарем, наприклад, ви перевіряєте на 0, тоді викидаєте виняток, і виклик потім заглядає в код о, цей клас може кинути виняток і т. Д. І чекати .. в цьому випадку це об'єкт ніколи не отримає 0, оскільки вони відфільтровані на 2 lvls раніше. Якщо це код бібліотеки, який повинен використовувати треті сторони, це вже інша історія. Не всі коди написані для використання у всьому світі.
Олександр Фулар

3

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

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

Іншими словами, те, що робить TDD, не заважає вам потребувати коду перевірки настільки ж, скільки допомагає вам не забувати його.


2

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

Мені здається, що аргумент такий:

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

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

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

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

Незважаючи на те, що ваш колега дещо менш досконалий неправильно, ніж більшість відповідей, я приходжу до того самого висновку, який полягає в тому, що дві техніки доповнюють одна одну. Програмуйте оборонно, задокументуйте свої захисні чеки та протестуйте їх. Робота є "непотрібною" лише в тому випадку, якщо користувачі вашого коду не можуть скористатися корисними повідомленнями про помилки, коли вони роблять помилки. Теоретично, якщо вони ретельно перевіряють весь свій код, перш ніж інтегрувати його з вашим, і в їхніх тестах ніколи не буде помилок, вони ніколи не побачать повідомлення про помилки. На практиці, навіть якщо вони роблять TDD та ін'єкцію тотальної залежності, вони все ще можуть досліджувати під час розвитку або може виникнути проміжок у їх тестуванні. У результаті вони називають ваш код до того, як їх код буде ідеальним!


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

@Craig: подивіться на це таким чином, якщо ви виділили компонент для тесту, глузуючи його залежності, то чому б ви не перевіряли, що він передає лише правильні значення цим залежностям? І якщо ви не можете виділити компонент, чи справді ви розділили проблеми? Я не погоджуюся з захисним кодуванням, але якщо захисні перевірки є засобом, за допомогою якого ви перевіряєте правильність виклику коду, то це безлад. Тож я думаю, що колега запитувача правильний, що чеки зайві, але неправильно вважати це причиною, щоб не писати їх :-)
Стів Джессоп,

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

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

1

Загальнодоступними інтерфейсами можна і буде зловживати

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

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


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

1

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

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

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

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

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

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

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

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

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

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


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

1

Тести визначають контракт вашого класу.

Як наслідок, відсутність тесту визначає контракт, який включає неозначене поведінку . Тож, коли ви переходите nullдо Foo::Frobnicate(Widget widget), і настає неперелічена тривалість запуску, ви все ще входите в договір свого класу.

Пізніше ви вирішите, "ми не хочемо можливості невизначеної поведінки", що є розумним вибором. Це означає, що ви повинні мати очікувану поведінку, щоб перейти nullдо Foo::Frobnicate(Widget widget).

І ви документуєте це рішення, включивши

[Test]
void Foo_FrobnicatesANullWidget_ThrowsInvalidArgument() 
{
    Given(Foo foo);
    When(foo.Frobnicate(null));
    Then(Expect_Exception(InvalidArgument));
}

1

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

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

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


0

Тести TDD виявлять помилки під час розробки коду .

Межі, які ви описуєте як частину оборонного програмування, дозволять помилитися під час використання коду .

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


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

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

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


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

@Craig Мені будуть цікаві ваші відгуки щодо конкретного прикладу, який я додав.
Blackhawk

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

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

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

0

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

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

uint8_t AddTwoBytes(uint8_t a, uint8_t b, uint8_t *sum); 

Тепер, якщо ви просто щойно *(sum) = a + bце зробили, це працювало б, але лише з деякими входами. a = 1і b = 2зробив би sum = 3; однак через те, що розмір суми є байтом, a = 100і b = 200зробить це sum = 44через переповнення. В C ви б повернули помилку в цьому випадку, щоб позначити функцію не вдалося; викидання виключення - це те саме, що у вашому коді. Не зважаючи на помилки чи тестування способів їх вирішення, це не спрацює довго, оскільки, якщо ці умови виникнуть, вони не будуть оброблятися і можуть спричинити будь-яку кількість проблем.


Це виглядає як хороший приклад запитання щодо інтерв'ю (чому він має значення повернення та параметр "out" - а що відбувається, коли sumє нульовий покажчик?).
Toby Speight
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.