Стоит ли тестировать частные методы или только публичные?
Я прочитал этот пост о том, как тестировать частные методы. Я обычно не проверяю их, потому что всегда думал, что быстрее тестировать только публичные методы, которые будут вызываться извне объекта. Вы тестируете частные методы? Я должен всегда проверять их?
28 ответов
Я не тестирую приватные методы. Закрытый метод - это деталь реализации, которая должна быть скрыта для пользователей класса. Тестирование частных методов нарушает инкапсуляцию.
Если я нахожу, что закрытый метод является огромным, сложным или достаточно важным, чтобы требовать своих собственных тестов, я просто помещаю его в другой класс и делаю его общедоступным ( метод Object). Затем я могу легко протестировать ранее приватный, но теперь публичный метод, который теперь живет в своем собственном классе.
Какова цель тестирования?
Большинство ответов пока говорят, что частные методы - это детали реализации, которые не имеют (или, по крайней мере, не должны) иметь значение, если публичный интерфейс хорошо протестирован и работает. Это абсолютно правильно, если ваша единственная цель для тестирования - гарантировать, что общедоступный интерфейс работает.
Лично я в основном использую тесты кода, чтобы гарантировать, что будущие изменения кода не вызовут проблем, и помочь моим усилиям по отладке, если они это сделают. Я считаю, что тестирование приватных методов так же тщательно, как и публичный интерфейс (если не больше!) Способствует этой цели.
Обратите внимание: у вас есть открытый метод A, который вызывает закрытый метод B. A и B оба используют метод C. C изменяется (возможно, вами, возможно, поставщиком), в результате чего A начинает проваливать свои тесты. Разве не было бы полезно иметь тесты на B также, хотя он и частный, чтобы вы знали, заключается ли проблема в том, что A использует C, B использует C или оба?
Тестирование частных методов также повышает ценность в тех случаях, когда тестовое покрытие открытого интерфейса является неполным. Несмотря на то, что мы обычно хотим избежать этой ситуации, тестирование эффективности блока зависит как от тестов, обнаруживающих ошибки, так и от связанных с этим затрат на разработку и обслуживание этих тестов. В некоторых случаях преимущества 100% покрытия тестами могут быть сочтены недостаточными, чтобы оправдать затраты на эти тесты, что приводит к пробелам в покрытии тестов общедоступного интерфейса. В таких случаях хорошо нацеленный тест частного метода может быть очень эффективным дополнением к базе кода.
Я склонен следовать советам Дейва Томаса и Энди Ханта в их книге " Прагматическое модульное тестирование":
В общем, вы не хотите нарушать инкапсуляцию ради тестирования (или, как говорила мама, "не выставляйте своих рядовых!"). В большинстве случаев вы должны иметь возможность тестировать класс, используя его общедоступные методы. Если за закрытым или защищенным доступом скрыта значительная функциональность, это может быть предупреждением о том, что существует другой класс, пытающийся выйти.
Но иногда я не могу удержаться от тестирования частных методов, потому что это дает мне чувство уверенности в том, что я создаю полностью надежную программу.
Я не люблю тестировать закрытые функции по нескольким причинам. Они заключаются в следующем (это основные моменты для людей TLDR):
- Обычно, когда вы испытываете желание протестировать приватный метод класса, это - запах проекта.
- Вы можете проверить их через общедоступный интерфейс (именно так вы хотите их протестировать, потому что именно так клиент будет их вызывать / использовать). Вы можете получить ложное чувство безопасности, увидев зеленый свет на всех проходящих тестах для ваших личных методов. Намного лучше / безопаснее тестировать крайние случаи на ваших частных функциях через ваш публичный интерфейс.
- Вы рискуете серьезным дублированием тестов (тесты, которые выглядят / выглядят очень похожими), тестируя частные методы. Это приводит к серьезным последствиям при изменении требований, так как будет проведено гораздо больше испытаний, чем необходимо. Это также может поставить вас в положение, в котором трудно провести рефакторинг из-за вашего набора тестов... что является абсолютной иронией, потому что набор тестов поможет вам безопасно перестроить и реорганизовать!
Я объясню каждый из них на конкретном примере. Оказывается, что 2) и 3) несколько запутанно связаны, поэтому их пример похож, хотя я рассматриваю их как отдельные причины, по которым вам не следует тестировать частные методы.
Есть один раз, когда я считаю подходящим тестирование частных методов, но я собираюсь рассмотреть его более подробно позже.
Я также расскажу, почему TDD не является оправданием для тестирования частных методов в самом конце.
Рефакторинг вашего выхода из плохого дизайна
Один из самых распространенных (анти) паттернов, который я вижу, - это то, что Майкл Фезерс называет классом "Айсберг" (если вы не знаете, кто такой Майкл Фезерс, то купите / прочитайте его книгу "Эффективная работа с устаревшим кодом". человек, которого стоит знать, если вы профессиональный инженер / разработчик программного обеспечения). Существуют и другие (анти) паттерны, которые приводят к возникновению этой проблемы, но на данный момент это самая распространенная проблема, с которой я столкнулся. У классов "Айсберг" есть один публичный метод, а остальные - частные (вот почему заманчиво тестировать приватные методы). Он называется классом "Айсберг", потому что обычно выявляется одинокий открытый метод, но остальная часть функций скрыта под водой в виде частных методов. Это может выглядеть примерно так:
Например, вы можете проверить 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:
Обратите внимание, что этот новый дизайн увеличивает модульность, так что вы можете потенциально использовать эти классы в других частях вашей системы (прежде чем вы не смогли, частные методы не могут быть повторно использованы по определению). Это является основным преимуществом разрушения 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
учебный класс:
Скажем так 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
класс, чтобы справиться с работой. Естественно, вы ожидаете, что один тест сломается, но эта боль усиливается, когда вы тестируете частные функции.
Когда может быть уместным тестирование частных методов?
В программном обеспечении нет "одного размера для всех". Иногда это нормально (и на самом деле идеально) "нарушать правила". Я настоятельно рекомендую не тестировать закрытые функции, когда это возможно. Есть две основные ситуации, когда я думаю, что все в порядке:
Я много работал с унаследованными системами (именно поэтому я такой большой поклонник Майкла Фезерса), и я могу с уверенностью сказать, что иногда просто безопаснее всего протестировать приватную функциональность. Это может быть особенно полезно для получения "тестов характеристик" в базовой линии.
Вы спешите, и вам нужно сделать как можно быстрее здесь и сейчас. В конце концов, вы не хотите тестировать частные методы. Но я скажу, что обычно требуется некоторое время на рефакторинг для решения проблем проектирования. И иногда вы должны отправить через неделю. Ничего страшного: делайте все быстро и грязно и тестируйте частные методы, используя инструмент поиска, если вы считаете, что это самый быстрый и надежный способ выполнить работу. Но поймите, что то, что вы сделали, было неоптимальным в долгосрочной перспективе, и, пожалуйста, рассмотрите возможность вернуться к нему (или, если об этом забыли, но вы увидите это позже, исправьте это).
Возможно, есть и другие ситуации, когда все в порядке. Если вы думаете, что все в порядке, и у вас есть хорошее оправдание, тогда сделайте это. Никто не останавливает вас. Просто будьте в курсе потенциальных затрат.
Оправдание TDD
Кроме того, я действительно не люблю людей, использующих TDD в качестве оправдания для тестирования частных методов. Я практикую TDD, и я не думаю, что TDD заставляет вас делать это. Вы можете сначала написать свой тест (для вашего открытого интерфейса), а затем написать код, удовлетворяющий этому интерфейсу. Иногда я пишу тест для общедоступного интерфейса, и я удовлетворяю его, написав также один или два небольших приватных метода (но я не тестирую приватные методы напрямую, но я знаю, что они работают, иначе мой публичный тест будет неудачным).). Если мне нужно протестировать крайние случаи этого закрытого метода, я напишу целую кучу тестов, которые будут проходить через мой открытый интерфейс. Если вы не можете понять, как добиться успеха в крайних случаях, это сильный знак того, что вам нужно реорганизовать небольшие компоненты, каждый со своими открытыми методами. Это признак того, что частные функции делают слишком много и выходят за рамки класса.
Кроме того, иногда я нахожу, что пишу тест, который слишком укусит, чтобы жевать в данный момент, и поэтому я думаю: "э, я вернусь к этому тесту позже, когда у меня будет больше API для работы" (я закомментирую и буду держать это в голове). Именно здесь многие разработчики, которых я встречал, начнут писать тесты для своей частной функциональности, используя TDD в качестве козла отпущения. Они говорят: "О, ну, мне нужен какой-то другой тест, но для написания этого теста мне понадобятся эти частные методы. Поэтому, поскольку я не могу написать производственный код без написания теста, мне нужно написать тест". для частного метода. " Но то, что им действительно нужно сделать, - это рефакторинг на более мелкие и повторно используемые компоненты вместо того, чтобы добавлять / тестировать кучу приватных методов в их текущий класс.
Замечания:
Я недавно ответил на аналогичный вопрос о тестировании частных методов с использованием GoogleTest. Я в основном изменил этот ответ, чтобы сделать его более независимым от языка.
PS Вот соответствующая лекция Майкла Фезерса о классах айсберга и инструментах поиска: https://www.youtube.com/watch?v=4cVZvoFGJTU
Я чувствую себя обязанным тестировать частные функции, поскольку все больше и больше следую одной из наших последних рекомендаций по обеспечению качества в нашем проекте:
Не более 10 по цикломатической сложности на функцию.
Теперь побочный эффект от применения этой политики состоит в том, что многие из моих очень больших публичных функций разделены на многие более сфокусированные, лучше названные частные функции.
Публичная функция все еще существует (конечно), но по существу сводится к тому, чтобы называть все эти частные "подфункциями"
Это на самом деле круто, потому что callstack теперь намного легче читать (вместо ошибки в большой функции, у меня есть ошибка в подподфункции с именем предыдущих функций в callstack, чтобы помочь мне понять "как я туда попал")
Однако теперь кажется, что проще выполнить модульное тестирование непосредственно этих закрытых функций и оставить тестирование большой общедоступной функции в каком-то "интеграционном" тесте, где необходимо рассмотреть сценарий.
Просто мои 2 цента.
Да, я тестирую частные функции, потому что, хотя они проверяются вашими общедоступными методами, в TDD (Test Driven Design) было бы неплохо протестировать наименьшую часть приложения. Но частные функции не доступны, когда вы находитесь в классе тестового модуля. Вот что мы делаем, чтобы проверить наши частные методы.
Почему у нас есть частные методы?
Закрытые функции в основном существуют в нашем классе, потому что мы хотим создать читаемый код в наших открытых методах. Мы не хотим, чтобы пользователь этого класса вызывал эти методы напрямую, а через наши открытые методы. Кроме того, мы не хотим менять их поведение при расширении класса (в случае защищенного), поэтому он является приватным.
Когда мы кодируем, мы используем тестируемый дизайн (TDD). Это означает, что иногда мы натыкаемся на частную функциональность и хотим протестировать. Закрытые функции не тестируются в phpUnit, потому что мы не можем получить к ним доступ в классе Test (они являются закрытыми).
Мы думаем, что есть 3 решения:
1. Вы можете проверить свои ряды с помощью публичных методов
преимущества
- Простое юнит-тестирование (не требуется взлом)
Недостатки
- Программист должен понимать публичный метод, в то время как он хочет только протестировать приватный метод
- Вы не тестируете самую маленькую тестируемую часть приложения
2. Если приватность так важна, возможно, это кодовая ячейка, чтобы создать для нее новый отдельный класс
преимущества
- Вы можете реорганизовать это в новый класс, потому что, если это так важно, это может понадобиться и другим классам.
- Тестируемый модуль теперь является публичным методом, поэтому тестируемый
Недостатки
- Вы не хотите создавать класс, если он не нужен, и используется только тем классом, откуда приходит метод
- Потенциальная потеря производительности из-за дополнительных накладных расходов
3. Измените модификатор доступа на (окончательный) защищенный
преимущества
- Вы тестируете самую маленькую тестируемую часть приложения. При использовании final protected функция не будет переопределена (как приватная)
- Без потери производительности
- Никаких дополнительных накладных расходов
Недостатки
- Вы изменяете частный доступ к защищенному, что означает, что он доступен для детей
- Вам все еще нужен класс Mock в вашем тестовом классе, чтобы использовать его
пример
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 для проверки нашей бывшей частной функции.
Я думаю, что лучше всего просто проверить открытый интерфейс объекта. С точки зрения внешнего мира, имеет значение только поведение открытого интерфейса, и именно на это должны быть направлены ваши юнит-тесты.
Как только у вас есть твердые модульные тесты, написанные для объекта, вам не нужно возвращаться и изменять эти тесты только потому, что изменилась реализация интерфейса. В этой ситуации вы нарушили последовательность вашего модульного тестирования.
Если ваш приватный метод не тестируется путем вызова ваших публичных методов, то что он делает? Я говорю частным, не защищенным или другом.
Если закрытый метод хорошо определен (т. Е. У него есть функция, которая может быть проверена и не должна изменяться со временем), тогда да. Я проверяю все, что можно проверить, где это имеет смысл.
Например, библиотека шифрования может скрывать тот факт, что она выполняет блочное шифрование с помощью закрытого метода, который шифрует только 8 байтов за раз. Я бы написал для этого модульный тест - он не предназначен для изменения, даже если он скрыт, и если он ломается (например, из-за будущих улучшений производительности), то я хочу знать, что сломалась частная функция, а не только что одна из общественных функций сломалась.
Это ускоряет отладку позже.
-Адам
Я не эксперт в этой области, но модульное тестирование должно проверять поведение, а не реализацию. Частные методы являются строго частью реализации, поэтому ИМХО не следует тестировать.
Если вы разрабатываете тестовое управление (TDD), вы будете тестировать свои частные методы.
Мы тестируем частные методы с помощью логического вывода, и я имею в виду, что мы ищем общий охват тестов класса, по крайней мере, на 95%, но наши тесты вызывают только публичные или внутренние методы. Чтобы получить покрытие, нам нужно сделать несколько звонков для общественности / внутренних органов в зависимости от возможных сценариев. Это делает наши тесты более внимательными к цели кода, который они тестируют.
Ответ Трампи на пост, на который вы ссылаетесь, является лучшим.
Модульные тесты, которые я считаю, предназначены для тестирования общедоступных методов. Ваши публичные методы используют ваши приватные методы, поэтому косвенно они также проходят тестирование.
Я некоторое время размышлял над этой проблемой, особенно когда попробовал свои силы в TDD.
Я натолкнулся на два сообщения, которые, я думаю, достаточно подробно решают эту проблему в случае TDD.
- Тестирование частных методов, TDD и тест-рефакторинг
- Разработка через тестирование - это не тестирование
В итоге:
При использовании методов разработки (проектирования) на основе тестирования частные методы должны возникать только в процессе перефакторинга уже работающего и протестированного кода.
По самой природе процесса любая часть простой функциональности реализации, извлеченная из тщательно протестированной функции, будет подвергнута самотестированию (т.е. охват косвенным тестированием).
Мне кажется достаточно ясным, что в начальной части кодирования большинство методов будут функциями более высокого уровня, потому что они инкапсулируют / описывают дизайн.
Поэтому эти методы будут общедоступными, и их тестирование будет достаточно простым.
Приватные методы появятся позже, когда все будет работать хорошо, и мы учитываем факторы для удобства чтения и чистоты.
Как указано выше: "Если вы не тестируете свои личные методы, откуда вы знаете, что они не сломаются?"
Это серьезная проблема. Одна из главных задач модульных тестов - знать, где, когда и как что-то сломалось как можно скорее. Таким образом, уменьшается значительный объем усилий по разработке и обеспечению качества. Если все, что проверяется, это публика, то у вас нет честного освещения и определения внутренних элементов класса.
Я нашел один из лучших способов сделать это - просто добавить ссылку на тест в проект и поместить тесты в класс, параллельный частным методам. Вставьте соответствующую логику сборки, чтобы тесты не встраивались в окончательный проект.
Тогда у вас есть все преимущества тестирования этих методов, и вы можете найти проблемы в считанные секунды, а не минуты или часы.
Итак, в заключение, да, юнит-тестирование ваших частных методов.
Вы не должны. Если ваши частные методы имеют достаточную сложность, которую необходимо протестировать, вы должны поместить их в другой класс. Сохраняйте высокую сплоченность, у класса должна быть только одна цель. Класс public интерфейса должен быть достаточно.
Если вы не тестируете свои личные методы, как вы узнаете, что они не сломаются?
Да, вы должны проверить частные методы, где это возможно. Зачем? Чтобы избежать ненужного взрыва в пространстве состояний тестовых случаев, которые в конечном итоге просто неявно тестируют одни и те же частные функции повторно на одних и тех же входах. Давайте объясним почему на примере.
Рассмотрим следующий слегка надуманный пример. Предположим, что мы хотим публично представить функцию, которая принимает 3 целых числа и возвращает true тогда и только тогда, когда все эти 3 целых числа просты. Мы могли бы реализовать это так:
public bool allPrime(int a, int b, int c)
{
return andAll(isPrime(a), isPrime(b), isPrime(c))
}
private bool andAll(bool... boolArray)
{
foreach (bool b in boolArray)
{
if(b == false) return false;
}
return true;
}
private bool isPrime(int x){
//Implementation to go here. Sorry if you were expecting a prime sieve.
}
Теперь, если мы будем придерживаться строгого подхода, согласно которому должны проверяться только публичные функции, нам будет разрешено тестировать только allPrime
и не isPrime
или же andAll
,
Как тестер, нас могут интересовать пять возможностей для каждого аргумента: < 0
, = 0
, = 1
, prime > 1
, not prime > 1
, Но чтобы быть тщательным, мы должны также увидеть, как каждая комбинация аргументов играет вместе. Так вот 5*5*5
= 125 тестовых случаев, нам нужно было бы тщательно протестировать эту функцию, согласно нашей интуиции.
С другой стороны, если бы нам было разрешено тестировать частные функции, мы могли бы охватить как можно больше с меньшим количеством тестовых случаев. Нам нужно всего 5 тестов isPrime
на том же уровне, что и наша предыдущая интуиция. И согласно гипотезе малого объема, предложенной Дэниелом Джексоном, нам нужно будет только проверить andAll
функционируют до малой длины, например 3 или 4. Что будет не более 16 тестов. Итого 21 тест. Вместо 125. Конечно, мы, вероятно, хотели бы провести несколько тестов на allPrime
, но мы бы не чувствовали себя настолько обязанными исчерпывающе охватить все 125 комбинаций входных сценариев, о которых мы говорили, что заботились о нас Всего несколько счастливых путей.
Придуманный пример, конечно, но это было необходимо для наглядной демонстрации. И шаблон распространяется на реальное программное обеспечение. Частные функции, как правило, являются строительными блоками самого низкого уровня и, таким образом, часто объединяются для получения логики более высокого уровня. То есть на более высоких уровнях у нас больше повторений вещей более низкого уровня из-за различных комбинаций.
Я понимаю точку зрения, когда частные методы рассматриваются как детали реализации, и тогда не нужно тестировать. И я бы придерживался этого правила, если бы нам пришлось развиваться только за пределами объекта. Но мы, мы какие-то ограниченные разработчики, которые разрабатывают только вне объектов, вызывая только свои публичные методы? Или мы на самом деле также развиваем этот объект? Поскольку мы не обязаны программировать внешние объекты, нам, вероятно, придется вызывать эти частные методы в новые открытые методы, которые мы разрабатываем. Разве не было бы замечательно знать, что закрытый метод противостоит всем шансам?
Я знаю, что некоторые люди могут ответить, что если мы разрабатываем другой публичный метод для этого объекта, то этот должен быть протестирован и все (частный метод может продолжать жить без теста). Но это также верно для любых открытых методов объекта: при разработке веб-приложения все открытые методы объекта вызываются из методов контроллеров и, следовательно, могут рассматриваться как подробности реализации для контроллеров.
Так почему же мы тестируем объекты? Поскольку это действительно трудно, нельзя сказать невозможно, быть уверенным, что мы тестируем методы контроллеров с соответствующим вводом, который будет запускать все ветви базового кода. Другими словами, чем выше мы в стеке, тем сложнее проверить все поведение. И то же самое для частных методов.
Для меня граница между частными и публичными методами - это психологический критерий, когда дело доходит до тестов. Критерии, которые имеют большее значение для меня:
- метод вызывается более одного раза из разных мест?
- метод достаточно сложен, чтобы требовать испытаний?
Это очевидно зависит от языка. В прошлом с C++ я объявил класс тестирования своим классом. К сожалению, для этого требуется, чтобы ваш производственный код знал о классе тестирования.
Публичное и частное не является полезным отличием того, что apis вызывать из ваших тестов, равно как и метод против класса. Большинство тестируемых модулей видны в одном контексте, но скрыты в других.
Важен охват и стоимость. Вам нужно минимизировать затраты при достижении целей покрытия вашего проекта (линия, ветвь, путь, блок, метод, класс, класс эквивалентности, сценарий использования... все, что решит команда).
Поэтому используйте инструменты для обеспечения покрытия и разрабатывайте свои тесты так, чтобы они приносили наименьшие затраты (краткосрочные и долгосрочные).
Не делайте тесты дороже, чем необходимо. Если это самое дешевое, то проверять только общедоступные точки входа. Если дешевле протестировать частные методы, сделайте это.
По мере того, как вы становитесь более опытным, вы будете лучше предсказывать, когда стоит проводить рефакторинг, чтобы избежать долгосрочных затрат на обслуживание тестов.
Я никогда не понимал концепцию модульного тестирования, но теперь я знаю, какова его цель.
Модульный тест не является полным тестом. Таким образом, это не замена для QA и ручного тестирования. Концепция TDD в этом аспекте неверна, поскольку вы не можете протестировать все, включая частные методы, но также и методы, которые используют ресурсы (особенно ресурсы, которые мы не контролируем). TDD базируется на всем своем качестве, это то, чего не могло быть достигнуто.
Юнит-тест - это скорее сводный тест. Вы помечаете произвольный пивот, и результат его должен остаться прежним.
Одним из основных моментов является
Если мы проверяем правильность логики, а частный метод несет логику, мы должны проверить ее. Не так ли? Так почему мы собираемся пропустить это?
Написание тестов, основанных на видимости методов, является совершенно неактуальной идеей.
Наоборот
С другой стороны, главная проблема заключается в вызове частного метода вне исходного класса. Кроме того, существуют ограничения для насмешки частного метода в некоторых инструментах насмешки. (Пример: Мокито)
Хотя есть некоторые инструменты, такие как Power Mock, которые поддерживают это, это опасная операция. Причина в том, что для этого нужно взломать JVM.
Можно обойти эту проблему (если вы хотите написать контрольные примеры для частных методов)
Объявите эти частные методы как защищенные. Но это может быть не удобно для нескольких ситуаций.
Если я нахожу, что закрытый метод является огромным, сложным или достаточно важным, чтобы требовать своих собственных тестов, я просто помещаю его в другой класс и делаю его общедоступным (метод Object). Затем я могу легко протестировать ранее закрытый, но теперь публичный метод, который теперь живет в своем собственном классе.
Если метод достаточно значительный / достаточно сложный, я обычно делаю его "защищенным" и проверяю его. Некоторые методы будут оставлены закрытыми и неявно проверены как часть модульных тестов для открытых / защищенных методов.
Нет. Вы не должны проверять частные методы, почему? и, кроме того, популярная среда моделирования, такая как Mockito, не поддерживает тестирование частных методов.
Вы также можете сделать свой метод package-private, то есть по умолчанию, и вы сможете использовать его для модульного тестирования, если только он не требуется для приватности.
Речь идет не только о публичных или частных методах или функциях, но и о деталях реализации. Частные функции - это только один аспект деталей реализации.
В конце концов, юнит-тестирование - это метод тестирования белого ящика. Например, тот, кто использует анализ покрытия для определения частей кода, которыми до сих пор пренебрегали при тестировании, углубляется в детали реализации.
А) Да, вы должны тестировать детали реализации:
Подумайте о функции сортировки, которая по соображениям производительности использует частную реализацию BubbleSort, если имеется до 10 элементов, и частную реализацию другого подхода сортировки (скажем, heapsort), если имеется более 10 элементов. Публичный API - это функция сортировки. Однако ваш набор тестов лучше использует знания о том, что на самом деле используются два алгоритма сортировки.
В этом примере, безусловно, вы можете выполнить тесты с открытым API. Это, однако, потребует наличия нескольких тестовых случаев, которые выполняют функцию сортировки с более чем 10 элементами, так что алгоритм heapsort достаточно хорошо протестирован. Наличие таких тестовых примеров само по себе указывает на то, что набор тестов связан с деталями реализации функции.
Если детали реализации функции сортировки изменятся, возможно, из-за того, что предел между двумя алгоритмами сортировки будет смещен, или что heapsort будет заменен на mergesort или что-то еще: существующие тесты будут продолжать работать. Тем не менее, их ценность сомнительна, и их, вероятно, необходимо переработать, чтобы лучше протестировать измененную функцию сортировки. Другими словами, будут предприняты усилия по обслуживанию, несмотря на то, что тесты проводились с использованием открытого API.
Б) Как проверить детали реализации
Одна из причин, почему многие люди утверждают, что не следует тестировать частные функции или детали реализации, заключается в том, что детали реализации с большей вероятностью изменятся. Эта более высокая вероятность изменений, по крайней мере, является одной из причин скрытия деталей реализации за интерфейсами.
Теперь предположим, что реализация интерфейса содержит более крупные частные части, для которых могут быть предусмотрены отдельные тесты внутреннего интерфейса. Некоторые люди утверждают, что эти части не должны быть проверены в частном порядке, они должны быть превращены во что-то публичное. После публичного юнит-тестирования этот код будет в порядке.
Это интересно: хотя интерфейс был внутренним, он, скорее всего, изменится, что является деталью реализации. Взяв тот же интерфейс, сделав его общедоступным, произойдет волшебное преобразование, а именно превращение его в интерфейс, который с меньшей вероятностью изменится. Очевидно, что в этой аргументации есть некоторый недостаток.
Но, тем не менее, в этом есть некоторая истина: при тестировании деталей реализации, в частности с использованием внутренних интерфейсов, следует стремиться использовать интерфейсы, которые, вероятно, останутся стабильными. Однако вероятность того, что какой-либо интерфейс будет стабильным, не просто зависит от того, является ли он общедоступным или закрытым. В проектах из мира, в которых я работал в течение некоторого времени, публичные интерфейсы также достаточно часто меняются, и многие частные интерфейсы остаются неизменными целую вечность.
Тем не менее, это хорошее эмпирическое правило, чтобы использовать "переднюю дверь первым" (см. http://xunitpatterns.com/Principles%20of%20Test%20Automation.html). Но имейте в виду, что он называется "сначала входная дверь", а не "только входная дверь".
C) Резюме
Проверьте также детали реализации. Предпочитают тестирование на стабильных интерфейсах (публичных или приватных). Если детали реализации меняются, также необходимо пересмотреть тесты общедоступного API. Превращение чего-то частного в публичное не может волшебным образом изменить его стабильность.
Ответ на вопрос "Должен ли я проверить частные методы?" это "....... иногда". Обычно вы должны тестировать интерфейс ваших классов.
- Одна из причин в том, что вам не нужно двойное покрытие для функции.
- Другая причина в том, что если вы изменяете приватные методы, вам придется обновлять каждый тест для них, даже если интерфейс вашего объекта не изменился вообще.
Вот пример:
class Thing
def some_string
one + two
end
private
def one
'aaaa'
end
def two
'bbbb'
end
end
class RefactoredThing
def some_string
one + one_a + two + two_b
end
private
def one
'aa'
end
def one_a
'aa'
end
def two
'bb'
end
def two_b
'bb'
end
end
В RefactoredThing
Теперь у вас есть 5 тестов, 2 из которых вам пришлось обновить для рефакторинга, но функциональность вашего объекта действительно не изменилась. Итак, допустим, что все более сложно, и у вас есть метод, который определяет порядок вывода, например:
def some_string_positioner
if some case
elsif other case
elsif other case
elsif other case
else one more case
end
end
Это не должно быть выполнено внешним пользователем, но ваш инкапсулирующий класс может быть слишком тяжелым, чтобы проходить через него столько логики снова и снова. В этом случае, может быть, вы бы предпочли извлечь это в отдельный класс, дать этому классу интерфейс и протестировать его.
И, наконец, допустим, что ваш основной объект очень тяжелый, а метод довольно маленький, и вам действительно нужно убедиться, что вывод правильный. Вы думаете: "Я должен проверить этот частный метод!". Возможно, вы можете сделать свой объект легче, передавая некоторые тяжелые работы в качестве параметра инициализации? Тогда вы можете передать что-то более легкое и протестировать против этого.
Я вижу, что многие люди придерживаются единого мнения: тестирование на общественном уровне. но разве это не то, что делает наша команда QA? Они проверяют ввод и ожидаемый результат. Если в качестве разработчиков мы тестируем только общедоступные методы, то мы просто переделываем работу QA и не добавляем никакой ценности путем "модульного тестирования".