Должен ли я избегать частных методов, если я выполняю TDD?

101

Я только сейчас изучаю TDD. Насколько я понимаю, частные методы не поддаются проверке и не должны беспокоиться, потому что общедоступный API предоставит достаточно информации для проверки целостности объекта.

Я понял ООП некоторое время. Насколько я понимаю, частные методы делают объекты более инкапсулированными, таким образом, более устойчивыми к изменениям и ошибкам. Таким образом, они должны использоваться по умолчанию, и только те методы, которые важны для клиентов, должны быть общедоступными.

Что ж, я могу создать объект, который имеет только частные методы и взаимодействует с другими объектами, слушая их события. Это было бы очень инкапсулировано, но совершенно не поддается проверке.

Также считается плохой практикой добавлять методы ради тестирования.

Значит ли это, что TDD расходится с инкапсуляцией? Каков соответствующий баланс? Я склонен обнародовать большинство или все мои методы сейчас ...

щенок
источник
9
Плохая практика и реальность в индустрии программного обеспечения - это разные животные. Идеальная ситуация чаще всего является искаженной реальностью в деловом мире. Делайте то, что имеет смысл, и придерживайтесь этого на протяжении всего приложения. Я бы предпочел иметь плохую практику, чем аромат месяца, разбросанный по приложению.
Аарон Макивер
10
"частные методы не проверяются"? На каком языке? На некоторых языках это неудобно. На других языках это совершенно просто. Кроме того, вы хотите сказать, что принцип проектирования инкапсуляции всегда должен реализовываться множеством частных методов? Это кажется немного экстремальным. Некоторые языки не имеют приватных методов, но, похоже, все еще имеют красиво инкапсулированные дизайны.
S.Lott
«Насколько я понимаю, частные методы делают объекты более инкапсулированными, что делает их более устойчивыми к изменениям и ошибкам. Таким образом, они должны использоваться по умолчанию, и только те методы, которые важны для клиентов, должны быть общедоступными». Это кажется мне противоположной точкой зрения на то, что TDD пытается достичь. TDD - это методология разработки, которая позволяет вам создавать простой, работоспособный и открытый для изменений дизайн. Взгляд «от частного» и «только публиковать ...» полностью оборачивается. Забудьте, что существует такая вещь, как закрытый метод для охвата TDD. Позже делайте их по мере необходимости как часть рефакторинга.
herby
Значит, @gnat, вы думаете, это должно быть закрыто как дубликат вопроса, который возник из моего ответа на этот вопрос? * 8 ')
Марк Бут

Ответы:

50

Предпочитайте тестирование интерфейса, а не тестирование на реализации.

Насколько я понимаю, частные методы не тестируются

Это зависит от вашей среды разработки, см. Ниже.

[частные методы] не должны беспокоиться, потому что общедоступный API предоставит достаточно информации для проверки целостности объекта.

Правильно, TDD фокусируется на тестировании интерфейса.

Частные методы - это детали реализации, которые могут меняться в течение любого цикла перефакторинга. Должна быть возможность перефакторинга без изменения интерфейса или поведения черного ящика . Фактически, это является частью преимущества TDD, легкость, с которой вы можете создать уверенность в том, что внутренние изменения в классе не повлияют на пользователей этого класса.

Что ж, я могу создать объект, который имеет только частные методы и взаимодействует с другими объектами, слушая их события. Это было бы очень инкапсулировано, но совершенно не поддается проверке.

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

Поскольку события являются интерфейсом, то именно эти события вам нужно будет генерировать для проверки этого объекта.

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

Также считается плохой практикой добавлять методы ради тестирования.

Безусловно, вы должны быть очень осторожны в разоблачении внутреннего состояния.

Значит ли это, что TDD расходится с инкапсуляцией? Каков соответствующий баланс?

Точно нет.

TDD не должен изменять реализацию ваших классов иначе как для упрощения их (применяя YAGNI с более ранней точки).

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

Я склонен обнародовать большинство или все мои методы сейчас ...

Это было бы лучше, если бы ребенок вылил воду в ванну.

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

Более детально рассмотрим тестирование приватных методов

Если вам абсолютно необходимо выполнить модульное тестирование некоторого личного поведения класса, в зависимости от языка / среды, у вас может быть три варианта:

  1. Поместите тесты в класс, который вы хотите проверить.
  2. Поместите тесты в другой файл класса / исходного кода и предоставьте приватные методы, которые вы хотите протестировать, как публичные методы.
  3. Используйте среду тестирования, которая позволяет разделить тестовый и рабочий код, но при этом разрешить тестировать доступ к частным методам рабочего кода.

Очевидно, что третий вариант является безусловно лучшим.

1) Поместите тесты в класс, который вы хотите протестировать (не идеально)

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

2) Выставьте приватные методы, которые вы хотите протестировать, как публичные (на самом деле не очень хорошая идея)

Как и предполагалось, это очень плохая практика, разрушает инкапсуляцию и раскрывает внутреннюю реализацию пользователям кода.

3) Используйте лучшую среду тестирования (лучший вариант, если он доступен)

В мире Eclipse, 3. может быть достигнуто с помощью фрагментов . В мире C # мы могли бы использовать частичные классы . Другие языки / среды часто имеют схожую функциональность, вам просто нужно ее найти.

Слепое предположение о том, что единственными вариантами являются 1. или 2., может привести к тому, что производственное программное обеспечение будет раздуто с помощью тестового кода или неприятных интерфейсов классов, которые стирают грязное белье в общественных местах. * 8' )

  • В общем, гораздо лучше не проверять частную реализацию.
Марк Бут
источник
5
Я не уверен, что согласился бы с любым из трех предложенных вами вариантов. Я бы предпочел только тестировать общедоступный интерфейс, как вы сказали ранее, но при этом использовать частные методы. Одним из преимуществ этого является поиск мертвого кода, что вряд ли произойдет, если вы заставите свой тестовый код нарушить нормальное использование языка.
Магус
Ваши методы должны делать одно, а тест не должен рассматривать реализацию каким-либо образом. Частные методы - это детали реализации. Если тестирование только общедоступных методов означает, что ваши тесты являются интеграционными тестами, у вас есть проблема разработки.
Магус
Как насчет того, чтобы сделать метод по умолчанию / защищенным и создать тест в тестовом проекте с тем же пакетом?
RichardCypher
@RichardCypher По сути, это то же самое, что и 2), поскольку вы в идеале изменяете спецификации своих методов для устранения недостатков в тестовой среде, поэтому, безусловно, все еще плохая практика.
Марк Бут
75

Конечно, вы можете иметь частные методы, и, конечно, вы можете проверить их.

Или есть какой - то способ , чтобы получить частный метод для запуска, в этом случае вы можете проверить это таким образом, или нет нет способа , чтобы получить частные бежать, и в этом случае: почему, черт возьми , вы пытаетесь проверить это, просто удалить эту чертову вещь!

В вашем примере:

Что ж, я могу создать объект, который имеет только частные методы и взаимодействует с другими объектами, слушая их события. Это было бы очень инкапсулировано, но совершенно не поддается проверке.

Почему это не поддается проверке? Если метод вызывается в ответ на событие, просто попросите тест передать объекту соответствующее событие.

Дело не в том, чтобы не иметь никаких закрытых методов, а в том, чтобы не нарушать инкапсуляцию. У вас могут быть частные методы, но вы должны проверить их через публичный API. Если открытый API основан на событиях, используйте события.

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

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

Йорг Миттаг
источник
3
Тестирование общедоступными методами работает хорошо в 99% случаев. Сложность заключается в 1% случаев, когда ваш единственный публичный метод имеет несколько сотен или тысяч строк сложного кода, а все промежуточные состояния зависят от реализации. Как только это становится достаточно сложным, попытка охватить все крайние случаи публичным методом становится в лучшем случае болезненной. С другой стороны, тестирование крайних вариантов путем либо нарушения инкапсуляции и предоставления большего количества методов как приватных, либо использования kludge, чтобы тесты вызывали частные методы, напрямую приводят к хрупким тестовым примерам в дополнение к уродливости.
Дэн Нили,
24
Большие, сложные частные методы - это запах кода. Реализация, которая настолько сложна, что ее невозможно эффективно разложить на составные части (с общедоступными интерфейсами), является проблемой в тестируемости, которая выявляет потенциальные проблемы проектирования и архитектуры. 1% случаев, когда частный код огромен, обычно выиграют от доработки и декомпозиции.
С.Лотт
13
@Dan Нили код, подобный этому, довольно непроверяем, независимо от того - и часть написания модульных тестов указывает на это. Устранить состояния, разбить классы, применить все типичные рефакторинги, а затем написать модульные тесты. Также с TDD, как ты вообще дошел до этого? Это одно из преимуществ TDD, написание тестируемого кода становится автоматическим.
Билл К
По крайней мере, internalметоды или публичные методы в internalклассах должны проверяться напрямую довольно часто. К счастью .net поддерживает InternalsVisibleToAttribute, но без него тестирование этого метода было бы PITA.
CodesInChaos
25

Когда вы создаете новый класс в своем коде, вы делаете это, чтобы отвечать некоторым требованиям. Требования говорят, что код должен делать, а не как . Это позволяет легко понять, почему большинство тестов происходит на уровне открытых методов.

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

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

И последнее, но не менее важное: в некоторых случаях вполне допустимо тестировать частные методы. Вот почему, например, в .NET вы можете тестировать не только публичные, но и частные классы, даже если решение не такое простое, как для открытых методов.

Арсений Мурзенко
источник
4
+1 Одна важная особенность TDD заключается в том, что она заставляет вас проверять выполнение ТРЕБОВАНИЙ, а не проверять, что МЕТОДЫ делают то, что, по их мнению, вы делаете. Таким образом, вопрос «Могу ли я протестировать частный метод» немного противоречит духу TDD - вместо этого вопрос может быть «Могу ли я проверить требование, реализация которого включает в себя частные методы». И ответ на этот вопрос однозначно да.
Дауд ибн Карим
6

Насколько я понимаю, частные методы не тестируются

Я не согласен с этим утверждением, или я бы сказал, что вы не тестируете частные методы напрямую . Открытый метод может вызывать разные частные методы. Возможно, автор хотел иметь «маленькие» методы и извлек часть кода в умно названный приватный метод.

Независимо от того, как написан открытый метод, ваш тестовый код должен охватывать все пути. Если после ваших тестов вы обнаружите, что один из операторов ветвления (if / switch) в одном частном методе никогда не охватывался вашими тестами, тогда у вас есть проблема. Либо вы пропустили случай, и реализация верна, либо реализация неправильна, и эта ветвь никогда не должна была существовать на самом деле.

Вот почему я часто использую Cobertura и NCover, чтобы убедиться, что мой тест открытых методов также охватывает частные методы. Не стесняйтесь писать хорошие ОО-объекты с закрытыми методами и не позволяйте TDD / Testing мешать вам в этом.

Jalayn
источник
5

Ваш пример все еще отлично тестируется, если вы используете Dependency Injection для предоставления экземпляров, с которыми взаимодействует ваша CUT. Затем вы можете использовать макет, сгенерировать интересующие события, а затем наблюдать, выполняет ли CUT правильные действия со своими зависимостями.

С другой стороны, если у вас есть язык с хорошей поддержкой событий, вы можете пойти немного по другому пути. Мне не нравится, когда объекты подписываются на сами события, вместо этого есть фабрика, которая создает объект, связывает события с открытыми методами объекта. Это проще для тестирования и делает его внешне видимым, для каких событий CUT должен быть проверен.

Крис Питман
источник
Это отличная идея ... "... иметь фабрику, которая создает объект, связывающий события с открытыми методами объекта. Это проще для тестирования и делает его внешне видимым, для каких типов событий необходимо проверять CUT. "
щенок
5

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

Это то, что я имею в виду, чтобы попытаться достичь работоспособного баланса.

  1. Минимизируйте количество частных методов и свойств, которые вы используете. Большинство вещей, которые нужны вашему классу, в любом случае, как правило, должны быть обнародованы, поэтому подумайте, действительно ли вам нужно сделать этот умный метод закрытым.
  2. Сведите к минимуму количество кода в ваших личных методах - вы действительно должны делать это в любом случае - и косвенно проверять, где вы можете, с помощью поведения других методов. Вы никогда не ожидаете получить 100% тестовое покрытие, и, возможно, вам придется вручную проверить несколько значений через отладчик. Использование закрытых методов для создания исключений может быть легко проверено косвенно. Частные свойства, возможно, должны быть проверены вручную или с помощью другого метода.
  3. Если косвенная или ручная проверка вас не устраивает, добавьте защищенное событие и доступ через интерфейс, чтобы раскрыть некоторые личные вещи. Это эффективно «изгибает» правила инкапсуляции, но избавляет от необходимости фактически отправлять код, который выполняет ваши тесты. Недостатком является то, что это может привести к небольшому дополнительному внутреннему коду, чтобы убедиться, что событие будет запущено при необходимости.
  4. Если вы чувствуете, что публичный метод недостаточно «безопасен», посмотрите, есть ли способы реализовать какой-либо процесс проверки в ваших методах, чтобы ограничить их использование. Скорее всего, пока вы думаете об этом, либо подумайте о лучшем способе реализации ваших методов, либо вы увидите, как другой класс начинает обретать форму.
  5. Если у вас есть много закрытых методов, выполняющих «вещи» для ваших открытых методов, возможно, существует новый класс, ожидающий извлечения. Вы можете проверить это непосредственно как отдельный класс, но реализовать его как отдельный составной класс внутри класса, который его использует.

Думай сбоку. Держите свои классы маленькими и ваши методы меньшими, и используйте много композиции. Это звучит как большая работа, но в итоге вы получите более индивидуально тестируемые элементы, ваши тесты будут проще, у вас будет больше возможностей для использования простых макетов вместо реальных, больших и сложных объектов, надеюсь, хорошо Фактор и слабосвязанный код, и что более важно, вы дадите себе больше возможностей. Уменьшение количества вещей, как правило, экономит ваше время, потому что вы сокращаете количество вещей, которые необходимо проверять отдельно для каждого класса, и вы, как правило, уменьшаете количество спагетти кода, которое иногда может произойти, когда класс становится большим и имеет много внутренне зависимое поведение кода.

S.Robins
источник
4

Что ж, я могу создать объект, который имеет только частные методы и взаимодействует с другими объектами, слушая их события. Это было бы очень инкапсулировано, но совершенно не поддается проверке.

Как этот объект реагирует на эти события? Предположительно, он должен вызывать методы для других объектов. Вы можете проверить это, проверив, вызваны ли эти методы. Пусть он вызывает фиктивный объект, и тогда вы легко можете утверждать, что он делает то, что вы ожидаете.

Проблема в том, что мы хотим проверить только взаимодействие объекта с другими объектами. Нам все равно, что происходит внутри объекта. Так что нет, у вас не должно быть больше публичных методов, чем раньше.

Уинстон Эверт
источник
4

Я тоже боролся с этой же проблемой. Действительно, способ обойти это так: как вы ожидаете, что остальная часть вашей программы будет взаимодействовать с этим классом? Проверьте свой класс соответственно. Это заставит вас создавать свой класс на основе того, как с ним взаимодействует остальная часть программы, и, фактически, будет стимулировать инкапсуляцию и хороший дизайн вашего класса.

шанс
источник
3

Вместо частного используйте модификатор по умолчанию. Затем вы можете тестировать эти методы индивидуально, а не только в сочетании с публичными методами. Это требует, чтобы ваши тесты имели ту же структуру пакета, что и ваш основной код.

siamii
источник
... при условии, что это Java.
Дауд ибн Карим
или internalв .net.
CodesInChaos
2

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

Например, у вас может быть обработчик событий, который выполняет много вызовов базы данных в ответ на эти события. Поскольку очевидно, что создание экземпляра обработчика событий для вызовов базы данных является плохой практикой, соблазн состоит в том, чтобы сделать все вызовы, связанные с базой данных, закрытыми методами, когда они действительно должны быть выделены в отдельный класс.

Карл Билефельдт
источник
2

Значит ли это, что TDD расходится с инкапсуляцией? Каков соответствующий баланс? Я склонен обнародовать большинство или все мои методы сейчас.

TDD не расходится с инкапсуляцией. Возьмите самый простой пример метода или свойства геттера, в зависимости от вашего языка. Допустим, у меня есть объект Customer, и я хочу, чтобы он имел поле Id. Первый тест, который я собираюсь написать, это что-то вроде «customer_id_initializes_to_zero». Я определяю метод получения, чтобы генерировать не реализованное исключение и наблюдать за ошибкой теста. Затем, самое простое, что я могу сделать, чтобы пройти этот тест, это вернуть получатель в ноль.

Оттуда я перейду к другим тестам, предположительно, с участием идентификатора клиента, являющегося фактическим функциональным полем. В какой-то момент мне, вероятно, придется создать приватное поле, которое класс клиента использует для отслеживания того, что должно быть возвращено получателем. Как именно я отслеживаю это? Это простая поддержка int? Отслеживать ли строку и затем преобразовывать ее в int? Я отслеживаю 20 дюймов и усредняю ​​их? Внешний мир не заботится - и ваши тесты TDD не заботятся. Это инкапсулированная деталь.

Я думаю, что это не всегда очевидно сразу при запуске TDD - вы не тестируете, что делают методы внутри себя - вы тестируете менее детальные проблемы класса. Таким образом, вы не хотите тестировать этот метод, создаете DoSomethingToFoo()экземпляр Bar, вызываете метод для него, добавляете два к одному из его свойств и т. Д. Вы проверяете, что после изменения состояния вашего объекта, некоторые средства доступа к состоянию изменились (или нет). Это общая схема ваших тестов: «когда я делаю X с моим тестируемым классом, я могу впоследствии наблюдать Y». Как добраться до Y, это не проблема тестов, и это то, что инкапсулировано, и именно поэтому TDD не расходится с инкапсуляцией.

Эрик Дитрих
источник
2

Избегать использования? Нет .
Избегайте , начиная с ? Да.

Я заметил, что вы не спрашивали о том, нормально ли иметь абстрактные классы с TDD; если вы понимаете, как возникают абстрактные классы во время TDD, тот же принцип применим и к закрытым методам.

Вы не можете напрямую тестировать методы в абстрактных классах так же, как вы не можете напрямую тестировать частные методы, но именно поэтому вы не начинаете с абстрактных классов и частных методов; вы начинаете с конкретных классов и общедоступных API, а затем реорганизуете обычную функциональность.


источник