Стоит ли тестировать частные методы или только публичные? - PullRequest
312 голосов
/ 19 сентября 2008

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

Ответы [ 30 ]

300 голосов
/ 19 сентября 2008

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

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

278 голосов
/ 20 сентября 2008

Какова цель тестирования?

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

Лично я в основном использую тесты кода, чтобы гарантировать, что будущие изменения кода не вызовут проблем, и помочь моим усилиям по отладке, если они это сделают. Я считаю, что тестирование приватных методов так же тщательно, как и публичный интерфейс (если не больше!), Способствует достижению этой цели.

Рассмотрим: у вас есть открытый метод A, который вызывает закрытый метод B. A и B оба используют метод C. C изменяется (возможно, вами, возможно, поставщиком), в результате чего A начинает проваливать свои тесты. Разве не было бы полезно иметь тесты на B также, хотя он и частный, чтобы вы знали, заключается ли проблема в том, что A использует C, B использует C или оба?

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

139 голосов
/ 20 сентября 2008

Я склонен следовать советам Дейва Томаса и Энди Ханта в их книге Прагматическое юнит-тестирование :

В общем, вы не хотите нарушать инкапсуляцию ради тестирование (или, как говорила мама, «не выставляйте своих рядовых!»). Наиболее времени, вы должны быть в состоянии проверить класс, выполняя его публичные методы. Если есть значительная функциональность, которая скрыта за частным или защищенным доступом, это может быть предупреждающим признаком того, что там есть другой класс, изо всех сил пытающийся выбраться.

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

58 голосов
/ 20 сентября 2008

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

Не более 10 в цикломатическая сложность на функцию.

Теперь побочный эффект от применения этой политики заключается в том, что многие из моих очень больших публичных функций разделены на многие более сфокусированные, лучше названные приватные функции.
Публичная функция все еще существует (конечно), но по существу сводится к тому, чтобы называть все эти частные «подфункции»

Это на самом деле круто, потому что callstack теперь намного легче читать (вместо ошибки в большой функции, у меня есть ошибка в подподфункции с именем предыдущих функций в callstack, чтобы помочь мне понять, «как я туда попал»)

Однако теперь кажется, что проще выполнить модульное тестирование непосредственно этих частных функций и оставить тестирование большой общедоступной функции в каком-то «интеграционном» тесте, где необходимо рассмотреть сценарий.

Только мои 2 цента.

50 голосов
/ 30 августа 2011

Да, я тестирую частные функции, потому что, хотя они тестируются вашими открытыми методами, в TDD (Test Driven Design) было бы неплохо протестировать наименьшую часть приложения. Но закрытые функции недоступны, когда вы находитесь в классе тестового модуля. Вот что мы делаем, чтобы проверить наши частные методы.

Почему у нас есть частные методы?

Закрытые функции в основном существуют в нашем классе, потому что мы хотим создать читаемый код в наших открытых методах. Мы не хотим, чтобы пользователь этого класса вызывал эти методы напрямую, а через наши открытые методы. Кроме того, мы не хотим изменять их поведение при расширении класса (в случае защищенного), поэтому он является приватным.

Когда мы кодируем, мы используем тест-дизайн (TDD). Это означает, что иногда мы сталкиваемся с частной функциональностью и хотим протестировать ее. Закрытые функции не тестируются в phpUnit, потому что мы не можем получить к ним доступ в классе Test (они являются закрытыми).

Мы думаем, что есть 3 решения:

1. Вы можете проверить свои ряды с помощью общедоступных методов

Преимущества

  • Простое юнит-тестирование (не требуется взлом)

Недостатки

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

2. Если приватность так важна, возможно, это кодовая ячейка для создания для нее нового отдельного класса

Преимущества

  • Вы можете изменить это на новый класс, потому что если это так важно, это может понадобиться и другим классам
  • Тестируемый модуль теперь является общедоступным методом, поэтому тестируемый

Недостатки

  • Вы не хотите создавать класс, если он не нужен, и используется только класс, откуда приходит метод
  • Потенциальная потеря производительности из-за дополнительных издержек

3. Измените модификатор доступа на (окончательный) защищенный

Преимущества

  • Вы тестируете самую маленькую тестируемую часть приложения. когда при использовании final protected функция не будет переопределена (просто как частный)
  • Без потери производительности
  • Без дополнительных затрат

Недостатки

  • Вы меняете частный доступ к защищенному, что означает доступно детям
  • Вам все еще нужен класс Mock в вашем тестовом классе, чтобы использовать его

* +1087 * Пример * ** тысяча восемьдесят-восемь * тысяча восемьдесят-девять

class Detective {
  public function investigate() {}
  private function sleepWithSuspect($suspect) {}
}
Altered version:
class Detective {
  public function investigate() {}
  final protected function sleepWithSuspect($suspect) {}
}
In Test class:
class Mock_Detective extends Detective {

  public test_sleepWithSuspect($suspect) 
  {
    //this is now accessible, but still not overridable!
    $this->sleepWithSuspect($suspect);
  }
}

Так что наш тестовый модуль теперь может вызывать test_sleepWithSuspect для проверки нашей прежней частной функции.

25 голосов
/ 21 ноября 2017

Я не люблю тестировать приватную функциональность по нескольким причинам. Они заключаются в следующем (это основные моменты для людей TLDR):

  1. Обычно, когда вы испытываете желание протестировать закрытый метод класса, это дизайнерский запах.
  2. Вы можете проверить их через публику интерфейс (который, как вы хотите, чтобы проверить их, потому что это как клиент будет звонить / использовать их). Вы можете получить ложное чувство безопасности, видя зеленый свет на всех проходящих тестах для вашего частного методы. Гораздо лучше / безопаснее тестировать крайние случаи в ваших личных функциях через открытый интерфейс.
  3. Вы рискуете серьезным дублированием теста (тесты, которые выглядят / чувствуются очень похоже) путем тестирования частных методов. Это имеет важное значение последствия, когда изменяются требования, столько же тестов, чем необходимо сломать. Это также может поставить вас в положение, когда это трудно реорганизовать из-за вашего набора тестов ... который является окончательным ирония, потому что тестовый набор поможет вам безопасно переделать и рефакторинг!

Я объясню каждый из них на конкретном примере. Оказывается, что 2) и 3) несколько запутанно связаны, поэтому их пример похож, хотя я считаю их отдельными причинами, по которым вам не следует тестировать частные методы.

Бывают случаи, когда уместно тестировать частные методы, просто важно знать о недостатках, перечисленных выше. Я расскажу об этом поподробнее позже.

Я также расскажу, почему TDD не является оправданием для тестирования частных методов в самом конце.

Рефакторинг вашего выхода из плохого дизайна

Один из самых распространенных (анти) паттернов, который я вижу, это то, что Майкл Фезерс называет классом "Айсберг" (если вы не знаете, кто такой Майкл Фезерс, иди купите / прочитайте его книгу «Эффективная работа с устаревшим кодом». Это человек, о котором стоит знать, если вы профессиональный инженер / разработчик программного обеспечения). Существуют и другие (анти) паттерны, которые приводят к возникновению этой проблемы, но на данный момент это самая распространенная проблема, с которой я столкнулся. У классов "Айсберг" есть один публичный метод, а остальные закрытые (вот почему заманчиво тестировать закрытые методы). Он называется классом «Айсберг», потому что обычно выявляется одинокий публичный метод, но остальная часть функций скрыта под водой в виде частных методов. Это может выглядеть примерно так:

Rule Evaluator

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

TEST_THAT(RuleEvaluator, canParseSpaceDelimtedTokens)
{
    input_string = "1 2 test bar"
    re = RuleEvaluator(input_string);

    ASSERT re.GetNextToken() IS "1";
    ASSERT re.GetNextToken() IS "2";
    ASSERT re.GetNextToken() IS "test";
    ASSERT re.GetNextToken() IS "bar";
    ASSERT re.HasMoreTokens() IS FALSE;
}

Ну, на самом деле это выглядит довольно мило. Мы хотели бы убедиться, что мы поддерживаем это поведение при внесении изменений. Но GetNextToken() является частной функцией! Так что мы не можем протестировать это так, , потому что он даже не скомпилирует (при условии, что мы используем какой-то язык, который на самом деле реализует public / private, в отличие от некоторых языков сценариев, таких как Python). Но как насчет изменения класса RuleEvaluator в соответствии с принципом единой ответственности (принцип единой ответственности)? Например, у нас, похоже, есть парсер, токенизатор и оценщик, объединенные в один класс. Не лучше ли разделить эти обязанности? Кроме того, если вы создадите класс Tokenizer, тогда его открытые методы будут HasMoreTokens() и GetNextTokens(). Класс RuleEvaluator может иметь в качестве члена объект Tokenizer. Теперь мы можем сохранить тот же тест, что и выше, за исключением того, что мы тестируем класс Tokenizer вместо класса RuleEvaluator.

Вот как это может выглядеть в UML:

Rule Evaluator Refactored

Обратите внимание, что этот новый дизайн увеличивает модульность, поэтому вы можете потенциально использовать эти классы в других частях вашей системы (раньше вы не могли использовать закрытые методы по определению). Это является основным преимуществом разрушения RuleEvaluator вместе с улучшенной понятностью / локальностью.

Тест выглядел бы чрезвычайно похожим, за исключением того, что он на самом деле компилируется на этот раз, так как метод GetNextToken() теперь открыт для класса Tokenizer:

TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
    input_string = "1 2 test bar"
    tokenizer = Tokenizer(input_string);

    ASSERT tokenizer.GetNextToken() IS "1";
    ASSERT tokenizer.GetNextToken() IS "2";
    ASSERT tokenizer.GetNextToken() IS "test";
    ASSERT tokenizer.GetNextToken() IS "bar";
    ASSERT tokenizer.HasMoreTokens() IS FALSE;
}

Тестирование частных компонентов через общедоступный интерфейс и предотвращение дублирования теста

Даже если вы не думаете, что можете разбить свою проблему на меньшее количество модульных компонентов (что вы можете сделать в 95% случаев, если просто попытаетесь сделать это), вы можете просто протестировать приватный функционирует через публичный интерфейс. Часто частные члены не стоит тестировать, потому что они будут протестированы через открытый интерфейс. Часто я вижу тесты, которые выглядят очень похожими, но тестируют две разные функции / методы. В конечном итоге происходит то, что когда требования меняются (а они всегда меняются), у вас теперь есть 2 неработающих теста вместо 1. И если вы действительно проверили все свои частные методы, у вас может быть больше 10 неработающих тестов вместо 1. Короче говоря, тестирование приватных функций (с помощью FRIEND_TEST или их публичное использование или рефлексия), которые в противном случае можно было бы протестировать через открытый интерфейс, может привести к дублированию теста . Вы действительно не хотите этого, потому что ничто не повредит больше, чем ваш набор тестов, замедляющий вас. Это должно сократить время разработки и снизить затраты на обслуживание! Если вы тестируете частные методы, которые в противном случае тестируются через открытый интерфейс, набор тестов вполне может сделать обратное и активно увеличить затраты на обслуживание и увеличить время разработки. Когда вы делаете приватную функцию общедоступной, или если вы используете что-то вроде FRIEND_TEST и / или рефлексии, вы, как правило, в конечном итоге пожалеете об этом в долгосрочной перспективе.

Рассмотрим следующую возможную реализацию класса Tokenizer:

enter image description here

Допустим, SplitUpByDelimiter() отвечает за возврат массива, так что каждый элемент в массиве является токеном. Кроме того, давайте просто скажем, что GetNextToken() - просто итератор по этому вектору. Итак, ваш публичный тест может выглядеть так:

TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
    input_string = "1 2 test bar"
    tokenizer = Tokenizer(input_string);

    ASSERT tokenizer.GetNextToken() IS "1";
    ASSERT tokenizer.GetNextToken() IS "2";
    ASSERT tokenizer.GetNextToken() IS "test";
    ASSERT tokenizer.GetNextToken() IS "bar";
    ASSERT tokenizer.HasMoreTokens() IS false;
}

Давайте представим, что у нас есть то, что Майкл Фезер называет нащупывающим инструментом . Это инструмент, который позволяет вам прикоснуться к частным частям других людей. Примером является FRIEND_TEST из googletest или отражение, если язык поддерживает это.

TEST_THAT(TokenizerTest, canGenerateSpaceDelimtedTokens)
{
    input_string = "1 2 test bar"
    tokenizer = Tokenizer(input_string);
    result_array = tokenizer.SplitUpByDelimiter(" ");

    ASSERT result.size() IS 4;
    ASSERT result[0] IS "1";
    ASSERT result[1] IS "2";
    ASSERT result[2] IS "test";
    ASSERT result[3] IS "bar";
}

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

Когда может подойти тестирование частных методов?

В программном обеспечении не существует "одного размера для всех". Иногда нормально (и на самом деле идеально) «нарушать правила». Я настоятельно рекомендую не тестировать закрытые функции, когда это возможно. Есть две основные ситуации, когда я думаю, что все в порядке:

  1. Я много работал с унаследованными системами (вот почему я такой большой поклонник Майкла Фезерса), и я могу с уверенностью сказать, что иногда просто безопаснее всего протестировать приватную функциональность. Это может быть особенно полезно для получения «тестов характеристик» в базовой линии.

  2. Вы спешите, и вам нужно сделать как можно быстрее здесь и сейчас. В конце концов, вы не хотите тестировать частные методы. Но я скажу, что обычно требуется некоторое время на рефакторинг для решения проблем проектирования. И иногда вы должны отправить через неделю. Ничего страшного: делайте все быстро и грязно и тестируйте частные методы, используя инструмент поиска, если это то, что, по вашему мнению, является самым быстрым и надежным способом выполнения работы. Но поймите, что то, что вы сделали, было неоптимальным в долгосрочной перспективе, и, пожалуйста, рассмотрите возможность вернуться к нему (или, если об этом забыли, но вы увидите это позже, исправьте это).

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

TDD Извините

Кроме того, мне действительно не нравятся люди, использующие TDD в качестве оправдания для тестирования частных методов. Я практикую TDD и не думаю, что TDD заставляет вас делать это. Вы можете сначала написать свой тест (для вашего открытого интерфейса), а затем написать код, удовлетворяющий этому интерфейсу. Иногда я пишу тест для общедоступного интерфейса, и я удовлетворяю его, написав также один или два небольших приватных метода (но я не тестирую приватные методы напрямую, но я знаю, что они работают, иначе мой публичный тест будет неудачным). ). Если мне нужно протестировать крайние случаи этого закрытого метода, я напишу целую кучу тестов, которые будут проходить через мой открытый интерфейс. Если вы не можете понять, как добиться успеха в крайних случаях, это сильный знак, который вам нужно реорганизовать в небольшие компоненты, каждый из которых имеет свои открытые методы. Это признак того, что частные функции слишком много делают и выходят за рамки класса .

Кроме того, иногда я нахожу, что пишу тест, который на данный момент слишком большой, чтобы жевать, и поэтому я думаю: «э, я вернусь к этому тесту позже, когда у меня будет больше API для работы с «(Я закомментирую это и буду держать это в уме). Именно здесь многие разработчики, которых я встречал, начнут писать тесты для своей частной функциональности, используя TDD в качестве козла отпущения. Они говорят: «О, ну, мне нужен какой-то другой тест, но для написания этого теста мне понадобятся эти закрытые методы. Поэтому, поскольку я не могу написать производственный код без написания теста, мне нужно написать тест». для частного метода. " Но то, что им действительно нужно сделать, - это рефакторинг на более мелкие и повторно используемые компоненты вместо добавления / тестирования множества частных методов в их текущий класс.

Примечание:

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

P.S. Вот соответствующая лекция Майкла Фезерса о классах айсберга и инструментах поиска: https://www.youtube.com/watch?v=4cVZvoFGJTU

24 голосов
/ 19 сентября 2008

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

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

19 голосов
/ 20 сентября 2008

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

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

Это ускоряет отладку позже.

-Adam

17 голосов
/ 20 сентября 2008

Если ваш приватный метод не тестируется путем вызова ваших публичных методов, то что он делает? Я говорю приват, не защищенный или друг.

12 голосов
/ 16 ноября 2009

Если вы разрабатываете тест-драйв (TDD), вы будете тестировать свои частные методы.

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