TDD та повне покриття тесту, де необхідні експоненціальні тестові випадки


18

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

  1. Точна відповідність на ім'я
  2. Усі слова пошукового запиту в імені або синонімі результату
  3. Деякі слова пошукового запиту в імені або синонімі результату (% у зменшенні)
  4. Усі слова пошукового запиту в описі
  5. Деякі слова пошукового запиту в описі (% у зменшенні)
  6. Остання змінена дата спадання

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

  1. 32
  2. 16
  3. 8 (оцінка вторинного вимикача на основі% спадання)
  4. 4
  5. 2 (оцінка вторинного вимикача на основі% спадання)
  6. 1

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

Фактичних тестів насправді буде менше. На підставі власне самих правил деякі правила забезпечують, що нижчі правила завжди будуть істинними (наприклад, коли "Усі слова пошукового запиту відображаються в описі", тоді правило "Деякі слова пошукового запиту з'являються в описі" завжди буде істинним). І все-таки варто докласти зусиль для написання кожного з цих тестових випадків? Це рівень тестування, який зазвичай вимагається, коли мова йде про 100% охоплення тесту в TDD? Якщо ні, то що було б прийнятною альтернативною стратегією тестування?


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

Відповіді:


17

Ваше запитання означає, що TDD має щось спільне з "спочатку написанням усіх тестових випадків". ІМХО це не "в дусі TDD", насправді це проти . Пам’ятайте, що TDD означає « тестово розвинуту розробку», тому вам потрібні лише ті тестові випадки, які дійсно «керують» вашою реалізацією, не більше. І поки ваша реалізація не розроблена таким чином, коли кількість блоків коду експоненціально зростає з кожною новою вимогою, вам також не знадобиться експоненціальна кількість тестових випадків. У вашому прикладі цикл TDD, ймовірно, буде виглядати приблизно так:

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

Потім почніть з 2-ї вимоги:

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

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

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


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

@maple_shaft: дякую, і мені дуже подобається ваше запитання. Мені подобається додати, що, мабуть, навіть при вашому підході до розробки всіх тестових випадків спочатку класична техніка побудови класів еквівалентності тестів може бути достатньою для зменшення експоненціального зростання (але я цього не працював поки що).
Док Браун

13

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

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

Тоді ви можете написати клас для кожної умови, і є лише 2 тести для кожного випадку.

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

public class ScoreBuilder
{
    private ISingleScorableCondition[] _conditions;
    public ScoreBuilder (ISingleScorableCondition[] conditions)
    {
        _conditions = conditions;
    }

    public int GetScore(string toBeScored)
    {
        foreach (var condition in _conditions)
        {
            if (_conditions.Test(toBeScored))
            {
                // score this somehow
            }
        }
    }
}

public class ExactMatchOnNameCondition : ISingleScorableCondition
{
    private IDataSource _dataSource;
    public ExactMatchOnNameCondition(IDataSource dataSource)
    {
        _dataSource = dataSource;
    }

    public bool Test(string toBeTested)
    {
        return _dataSource.Contains(toBeTested);
    }
}

// etc

Ви помітите, що ваші 2 ^ тести на умови швидко знижуються до 4+ (2 * умови). 20 набагато менше владних, ніж 64. І якщо ви додасте ще один пізніше, вам не доведеться змінювати БУДЬ-кого з існуючих класів (відкритий-закритий принцип), тому вам не доведеться писати 64 нові тести, просто у вас є додати ще один клас з 2 новими тестами та ввести його у свій клас ScoreBuilder.


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

4
@maple_shaft: Ні, але ви шукали поради TDD, і такі алгоритми ідеально підходять для усунення питання, чи варто того докладати зусиль, значно скорочуючи зусилля. Зменшення складності є ключовим фактором TDD.
pdr

+1, чудова відповідь. Хоча я вважаю, що навіть без такого складного рішення кількість тестових випадків не повинна зростати експоненціально (див. Мою відповідь нижче).
Док Браун

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

4

І все-таки варто докласти зусиль для написання кожного з цих тестових випадків?

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

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

63-й тест, мабуть, не вартий, тому що це те, на що ви впевнені 99,99%, охоплене логікою вашого коду чи іншим тестом.

Це рівень тестування, який зазвичай вимагається, коли мова йде про 100% охоплення тесту в TDD?

Я розумію, що 100% охоплення означає, що всі кодові шляхи виконуються. Це не означає, що ви виконуєте всі комбінації своїх правил, але всі різні шляхи коду можуть піти вниз (як ви зазначаєте, деякі комбінації не можуть існувати в коді). Але оскільки ви робите TDD, ще немає "коду" для перевірки шляхів. У листі цього процесу можна сказати, що всі 63+.

Особисто я вважаю, що 100% покриття є мрією про трубу. Крім того, це непрагматично. Єдині тести існують, щоб служити вам, а не навпаки. Коли ви робите більше тестів, ви отримуєте зменшення прибутку від вигоди (ймовірність того, що тест запобігає помилку + впевненість у правильності коду). Залежно від того, що ваш код, визначає, де на цій ковзній шкалі ви припиняєте робити тести. Якщо у вашому коді працює ядерний реактор, можливо, всі 63+ випробування варті того. Якщо ваш код упорядковує ваш музичний архів, то, ймовірно, ви можете піти набагато менше.


"Покриття" зазвичай стосується покриття коду (виконується кожен рядок коду) або покриття гілки (кожна гілка виконується принаймні один раз у будь-якому можливому напрямку). Для обох типів покриття не потрібно 64 різних тестових випадків. Принаймні, не з серйозною реалізацією, яка не містить окремих кодових частин для кожного з 64 випадків. Тож 100% покриття цілком можливо.
Док Браун

@DocBrown - впевнений, що в цьому випадку - інші речі важче / неможливо перевірити; розглянути шляхи виключення з пам'яті. Чи не всі 64 потрібні в "літері" TDD для приведення в дію поведінки випробовується невідомо про реалізацію?
Теластин

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

4

Я б стверджував, що це ідеально випадок для TDD.

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

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


1

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

Маючи на увазі цю ідею, я б вичерпно перевірив 6 факторів ранжування, які ви перераховували ізольовано один до одного з подальшими 2 або 3 тестами стилю інтеграції, які забезпечують зведення результатів до очікуваних загальних значень рейтингу. Наприклад, випадок №1, "Точне збіг на ім'я", я мав би принаймні два одиничні тести, щоб перевірити його точність, а коли - ні, і щоб два сценарії повернули очікуваний бал. Якщо він відрізняється від регістру, то також випадок тестування "Точного відповідника" та "точного відповідності" та, можливо, інших варіантів введення, таких як пунктуація, додаткові пробіли тощо, також повертає очікувані бали.

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

Якщо припустити, що випадки №2 / №3 та №4 / №5 узагальнені до одних і тих же базових методів, але, передаючи різні поля в, вам потрібно написати лише один набір одиничних тестів для базових методів і написати прості додаткові одиничні тести для перевірки конкретних поля (назва, ім'я, опис тощо) та оцінка за визначеним факторингом, тому це ще більше зменшує надмірність ваших загальних тестових зусиль.

При такому підході вищеописаний підхід, ймовірно, дасть 3 або 4 одиничні тести на випадок №1, можливо, 10 специфікацій на деякі / всі w / синоніми враховуються - плюс 4 специфікації щодо правильного оцінювання справ №2 - №5 та 2 до 3-х специфікацій на кінцеву дату впорядкованого ранжирування, потім від 3 до 4-х тестових рівнів інтеграції, які вимірюють усі 6 випадків, об'єднаних імовірними способами (забудьте про незрозумілі крайові випадки поки що, якщо ви чітко не бачите проблеми у своєму коді, яку потрібно застосувати для забезпечення ця умова обробляється) або переконайтеся, що вона не буде порушена / порушена пізнішими переглядами. Це дає приблизно 25 або більше специфікацій для здійснення 100% написаного коду (навіть якщо ви безпосередньо не називали 100% написаних методів).


1

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

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

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

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

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