Що відбувається з тестами методів, коли цей метод стає приватним після перепроектування в TDD?


29

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

Застосовуючи TDD, я роблю кілька тестових випадків, щоб перевірити логіку всередині Character.receiveAttack(Int)методу. Щось на зразок цього:

@Test
fun healthIsReducedWhenCharacterIsAttacked() {
    val c = Character(100) //arg is the health
    c.receiveAttack(50) //arg is the suffered attack damage
    assertThat(c.health, is(50));
}

Скажіть, у мене є 10 методів тестування receiveAttack. Тепер я додаю метод Character.attack(Character)(який викликає receiveAttackметод), і після деяких циклів TDD тестуючи його, я приймаю рішення: Character.receiveAttack(Int)повинен бути private.

Що відбувається з попередніми 10 тестовими випадками? Чи потрібно їх видалити? Чи варто дотримуватися методу public(я не думаю, що так)?

Це питання не в тому, як перевірити приватні методи, а як боротися з ними після перепроектування при застосуванні TDD



10
Якщо це приватне, ви не тестуєте це, це так просто. Вийміть та виконайте танець
Refactor

6
Я, мабуть, іду проти зерна. Але я взагалі уникаю приватних методів за будь-яку ціну. Я віддаю перевагу більше тестів, ніж менше тестів. Я знаю, що люди думають "Що, так що у вас ніколи не існує функціоналу, якого ви не хочете піддавати споживачеві?". Так, у мене є багато, чого я не хочу виставляти. Натомість, коли у мене є приватний метод, я замість цього перетворюю його на власний клас та використовую зазначений клас із початкового класу. Новий клас може бути позначений як internalабо еквівалент вашої мови, щоб все-таки запобігти його викриттю. Насправді відповідь Кевіна Клайна полягає в такому підході.
user9993

3
@ user9993 ти, схоже, маєш це назад. Якщо вам важливо мати більше тестів, єдиний спосіб переконатися, що ви нічого не пропустили, - це запустити аналіз покриття. А для інструментів покриття це зовсім не має значення, метод є приватним чи загальнодоступним чи ще. Сподіваючись, що оприлюднення речей якось компенсує відсутність аналізу покриття, я боюсь помилкового відчуття безпеки
gnat

2
@gnat Але я ніколи нічого не говорив про те, щоб "не мати покриття"? Мій коментар про "я віддаю перевагу більше тестів, ніж менше тестів" повинен був зробити це очевидним. Не впевнений, що ви точно отримуєте, звичайно, я також перевіряю діставаний вами код. У цьому вся суть.
user9993

Відповіді:


52

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

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

А якщо цього немає, то функціонал в receiveAttackцьому більше не потрібен і більше не повинен існувати!

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


14
Це хороша відповідь, окрім "Якщо ваша тестова рамка спрощує тестування приватних методів, а якщо ви вирішите протестувати приватні методи, то можете їх зберегти". Приватні методи - це деталі реалізації, і їх ніколи не слід безпосередньо перевіряти.
Девід Арно

20
@DavidArno: Я не згоден, тому міркування внутрішні модулі ніколи не повинні перевірятися. Однак внутрішні модулі можуть мати велику складність, і тому наявність тестових одиниць для кожної окремої внутрішньої функціональності може бути цінною. Блок-тести використовуються для перевірки інваріантів частини функціональності, якщо приватний метод має інваріанти (попередні умови / умови), то одиничний тест може бути цінним.
Матьє М.

8
" завдяки цьому міркуванню внутрішні модулі ніколи не повинні перевірятися ". Ці внутрішні органи ніколи не повинні бути безпосередньо перевірені. Усі тести повинні перевіряти лише загальнодоступні API. Якщо внутрішній елемент недоступний через загальнодоступний API, видаліть його, оскільки він нічого не робить.
Девід Арно

28
@DavidArno За цією логікою, якщо ви будуєте виконуваний файл (а не бібліотеку), у вас взагалі не повинно бути одиничних тестів. - "Виклики функцій не є частиною загальнодоступного API! Тільки аргументи командного рядка є! Якщо внутрішня функція вашої програми недоступна через аргумент командного рядка, видаліть її, оскільки вона нічого не робить." - Хоча приватні функції не є частиною загальнодоступного API класу, вони є частиною внутрішнього API класу. І хоча вам не обов’язково потрібно тестувати внутрішній API класу, ви можете, використовуючи ту саму логіку для тестування внутрішнього API виконуваного файлу.
RM

7
@RM, якби я створив виконуваний файл немодульним способом, я був би змушений вибирати між крихкими тестами внутрішніх справ або використовувати лише тести інтеграції з використанням виконуваного і вводу-виводу виконання. Тому за моєю фактичною логікою, а не за вашою версією соломенника, я б створив його модульно (наприклад, через набір бібліотек). Потім громадські API цих модулів можуть бути протестовані неміцним способом.
Девід Арно

23

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

public class X {
  private int complexity(...) {
    ...
  }
  public void somethingElse() {
    int c = complexity(...);
  }
}

до:

public class Complexity {
  public int calculate(...) {
    ...
  }
}

public class X {
  private Complexity complexity;
  public X(Complexity complexity) { // dependency injection happiness
    this.complexity = complexity;
  }

  public void something() {
    int c = complexity.calculate(...);
  }
}

Перемістіть поточний тест на X.complexity на ComplexityTest. Потім текст X. щось щось, глузуючи зі Складності.

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


Ваша відповідь набагато чіткіше пояснює ідею, яку я намагався пояснити, коментуючи питання ОП. Гарна відповідь.
user9993

3
Дякую за вашу відповідь. Насправді метод receAttack досить простий ( this.health = this.health - attackDamage). Можливо, для цього моменту витягнути його до іншого класу - це надмірно розроблене рішення.
Гектор

1
Це, безумовно, надмірний рівень для ОП - він хоче заїхати до магазину, а не полетіти на Місяць - але гарне рішення в загальному випадку.

Якщо функція є такою простою, можливо, це переосмислення, навіть її визначають як функцію.
David K

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

6

Скажіть, у мене є 10 методів тестування методом receAttack. Тепер я додаю метод Character.attack (Character) (який викликає метод receAttack), і після деяких циклів тестування його TDD я приймаю рішення: Character.receiveAttack (Int) повинен бути приватним.

Тут слід пам’ятати, що рішення, яке ви приймаєте, - це видалити метод з API . Ввічливість зворотної сумісності підказала б

  1. Якщо вам не потрібно його видаляти, залиште його в API
  2. Якщо вам не потрібно , щоб видалити його ще , то помітити його як застарілий і , якщо можливо , коли документ до кінця життя буде
  3. Якщо вам потрібно його видалити, то у вас є основна зміна версії

Тести видаляються / або замінюються, коли ваш API більше не підтримує метод. У цей момент приватний метод - це детальна інформація про реалізацію, яку ви повинні мати можливість рефакторировать.

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


3
Депресія не завжди викликає занепокоєння. З питання: "Скажімо, я починаю розробляти ...", якщо програмне забезпечення ще не випущено, депресія - це не проблема. Крім того: "рольова гра" означає, що це не бібліотека для багаторазового використання, а бінарне програмне забезпечення, спрямоване на кінцевих споживачів. Хоча деякі програмні засоби для кінцевих користувачів мають публічний API (наприклад, MS Office), у більшості немає. Навіть програмне забезпечення , яке робить загальнодоступний API має тільки частина її відкритою для плагінів, скриптів (наприклад , ігри з розширенням LUA), або інші функції. І все ж, варто викласти ідею для загальної справи, яку описує ОП.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.