Что происходит с тестами методов, когда этот метод становится приватным после перепроектирования в 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

Эктор
источник
2
Возможный дубликат тестирования частных методов, защищаемой
комар
10
Если это личное, ты не проверяешь это, это так просто. Удалить и сделать танец рефактора
kayess
6
Я, вероятно, иду против зерна здесь. Но я вообще избегаю частных методов любой ценой. Я предпочитаю больше тестов, чем меньше тестов. Я знаю, что думают люди: «Что, так что у вас никогда не будет никакой функциональности, которую вы не хотите показывать потребителю?». Да, у меня есть много вещей, которые я не хочу раскрывать. Вместо этого, когда у меня есть закрытый метод, я вместо этого реорганизую его в свой собственный класс и использую указанный класс из исходного класса. Новый класс может быть помечен как internalили эквивалент вашего языка для предотвращения его раскрытия. На самом деле ответ Кевина Клайна - это такой подход.
user9993
3
@ user9993 у тебя, кажется, это задом наперед. Если для вас важно иметь больше тестов, единственный способ убедиться, что вы не пропустили ничего важного, - запустить анализ покрытия. А для инструментов покрытия вообще не имеет значения, является ли метод частным или общедоступным или что-то еще. Надеясь , что делает вещи общественность каким - то образом компенсировать отсутствие анализа покрытия дает ложное чувство безопасности , я боюсь
комара
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.something, насмешливо Сложность.

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

Кевин Клайн
источник
Ваш ответ гораздо яснее объясняет идею, которую я пытался объяснить в своем комментарии к вопросу ОП. Хороший ответ.
user9993
3
Спасибо за Ваш ответ. На самом деле, метод receiveAttack довольно прост ( this.health = this.health - attackDamage). Возможно, извлечь его в другой класс - это слишком силовое решение, на данный момент.
Гектор
1
Это определенно излишне для ОП - он хочет ехать в магазин, а не летать на Луну, - но хорошее решение в общем случае.
Если функция настолько проста, то, возможно, это слишком сложная задача, что она вообще определяется как функция.
Дэвид К
1
это может быть излишним сегодня , но во время 6 месяцев, когда есть тонна изменений этого кода выгода будет ясно. И в любой приличной среде IDE в наши дни, безусловно, извлечение некоторого кода в отдельный класс должно быть парой нажатий клавиш, что вряд ли является чрезмерно сложным решением, учитывая, что в двоичном коде во время выполнения все это сводится к одному и тому же.
Стивен Бирн
6

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

Здесь следует иметь в виду, что решение, которое вы принимаете, заключается в удалении метода из API . Любезность обратной совместимости предполагает

  1. Если вам не нужно удалять его, оставьте его в API
  2. Если вам не нужно , чтобы удалить его еще , то пометить его как устаревший и , если возможно , когда документ до конца жизни будет
  3. Если вам нужно удалить его, то у вас есть серьезное изменение версии

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

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

VoiceOfUnreason
источник
3
Устаревание не всегда вызывает беспокойство. Из вопроса: «Допустим, я начинаю разработку ...», если программное обеспечение еще не выпущено, устаревание не является проблемой. Более того: «ролевая игра» подразумевает, что это не многократно используемая библиотека, а двоичное программное обеспечение, предназначенное для конечных пользователей. В то время как некоторые программы конечного пользователя имеют общедоступный API (например, MS Office), большинство не имеет. Даже программное обеспечение , которое делает общедоступный API имеет только часть ее открытой для плагинов, скриптов (например , игры с расширением LUA), или другие функции. Тем не менее, стоит поднять идею для общего случая, который описывает ОП.