Боротьба з циклічними залежностями в одиничних тестах


24

Я намагаюся практикувати TDD, використовуючи його для розробки такого простого, як Bit Vector. Я випадково використовую Swift, але це мовно-агностичне питання.

Моє BitVector- це structзберігання синглів UInt64і презентує API, який дозволяє вам ставитися до нього як до колекції. Деталі не мають великого значення, але це досить просто. Високі 57 біт - це біти зберігання, а нижні 6 - це "рахувати" біти, що говорить вам, скільки бітів для зберігання насправді зберігають міститься значення.

Поки що у мене є кілька дуже простих можливостей:

  1. Ініціалізатор, який створює порожні бітові вектори
  2. countвластивість типуInt
  3. isEmptyвластивість типуBool
  4. Оператор рівності ( ==). Примітка: це оператор рівності значень, схожий на Object.equals()Java, а не оператор еталонної рівності, як ==у Java.

Я натрапляю на купу циклічних залежностей:

  1. Тест одиниці, який тестує мій ініціалізатор, повинен перевірити, що щойно побудований BitVector. Це можна зробити одним із трьох способів:

    1. Перевірка bv.count == 0
    2. Перевірка bv.isEmpty == true
    3. Перевірте це bv == knownEmptyBitVector

    Метод 1 спирається на countметод 2, на isEmptyякий покладається (на який він покладається count, тому немає сенсу використовувати його), метод 3 спирається на ==. У будь-якому випадку, я не можу перевірити свій ініціалізатор ізольовано.

  2. Тест на countнеобхідність оперувати чимось, що неминуче тестує мої ініціалізатори

  3. Реалізація isEmptyпокладається наcount

  4. Реалізація ==покладається на count.

Мені вдалося частково вирішити цю проблему, ввівши приватний API, який будує a BitVectorз існуючого бітового шаблону (як a UInt64). Це дозволило мені ініціалізувати значення, не тестуючи жодних інших ініціалізаторів, щоб я міг "завантажувати ремінь" вперед.

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

Як саме ви обходите подібні питання?


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

Ви знаєте занадто багато деталей щодо впровадження наперед. Чи справді ваш розвиток орієнтований на тести ?
herby

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

@Alexander Ви повинні спробувати розслабитися, інакше це буде тестовим першим, але не тестовим. Просто скажіть розпливчасто: "Я зроблю біт-вектор з одним 64-бітовим інтом як резервний магазин", і все; з цього моменту зробіть TDD червоно-зелений рефактор один за одним. Деталі щодо впровадження, як і API, повинні з'являтися від спроб зробити тести (першими) та написання цих тестів в першу чергу (останніх).
herby

Відповіді:


66

Ви надто переживаєте про деталі впровадження.

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

  • Що має щойно ініціалізований об’єкт count == 0.
  • Що має щойно ініціалізований об’єкт isEmpty == true
  • Що нещодавно ініціалізований об'єкт дорівнює відомому порожньому об'єкту.

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

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


6
@Alexander Ви схожі на людину, яка потребує чіткого визначення одиничного тестування. Найкраще, що я знаю, походить від Майкла Пір'я
candied_orange

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

9
@ Олександр "шматок коду" - це довільне вимірювання. Просто ініціалізуючи змінну, ви використовуєте багато "фрагментів коду". Важливо те, що ви протестуєте згуртовану поведінкову одиницю, як визначено вами .
Мураха P

9
"З того, що я прочитав, у мене склалося таке враження, що якщо ви порушите лише фрагмент коду, не вдасться виконати лише одиничні тести, безпосередньо пов'язані з цим кодом." Це здається дуже важким правилом для дотримання. (наприклад, якщо ви пишете векторний клас, і ви помиляєтесь в методі індексу, ви, ймовірно, матимете багато зламів у всьому коді, який використовує цей векторний клас)
jhominal

4
@Alexander Також вивчіть схему "Впорядкувати, діяти, оцінити" для тестів. В основному ви встановлюєте об'єкт у будь-якому стані, в якому він повинен знаходитись (Упорядкувати), називаєте метод, який ви насправді тестуєте (Act), а потім перевіряєте, чи змінився його стан відповідно до ваших очікувань. (Затвердження). Те, що ви створили в Arrange, було б "передумовами" для тесту.
GalacticCowboy

5

Як саме ви обходите подібні питання?

Ви переглядаєте своє мислення щодо того, що таке "одиничний тест".

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

На практиці це часто виглядає так

// GIVEN
obj = new Object(...)

// THEN
assert object.read(...)

або

// GIVEN
obj = new Object(...)

// WHEN
object.change(...)

// THEN
assert object.read(...)

Термінологія "тестова одиниця" - ну, вона має довгу історію не дуже хорошого.

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

Кент написав першу версію SUnit у 1994 році , порт на JUnit був у 1998 році, перший проект книги TDD був на початку 2002 року. Плутанина мала багато часу поширитися.

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

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

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


1

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

Якщо ви справді робите TDD, це вже має бути так. Ваші тести - це виконана специфікація програми.

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

Я думаю, що вашою проблемою є ваше розуміння TDD.

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

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

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