Я не люблю тестировать закрытые функции по нескольким причинам. Они заключаются в следующем (это основные моменты для людей 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