Насколько я понимаю, большинство людей, похоже, согласны с тем, что частные методы следует тестировать не напрямую, а с помощью любых открытых методов, которые их вызывают. Я вижу их точку зрения, но у меня возникают некоторые проблемы с этим, когда я пытаюсь следовать «трем законам TDD» и использовать цикл «красный - зеленый - рефакторинг». Я думаю, что это лучше всего объяснить на примере:
Сейчас мне нужна программа, которая может прочитать файл (содержащий данные, разделенные табуляцией) и отфильтровать все столбцы, которые содержат нечисловые данные. Я думаю, что, вероятно, уже есть несколько простых инструментов для этого, но я решил реализовать их с нуля, в основном потому, что решил, что это может быть хорошим и чистым проектом для меня, чтобы немного попрактиковаться в TDD.
Итак, во-первых, я «надеваю красную шляпу», то есть мне нужен тест, который не проходит. Я подумал, мне понадобится метод, который находит все нечисловые поля в строке. Поэтому я пишу простой тест, конечно, он не может быть скомпилирован немедленно, поэтому я начинаю писать саму функцию, и после пары циклов назад и вперед (красный / зеленый) у меня есть рабочая функция и полный тест.
Далее, я продолжаю с функцией «collectNonNumericColumns», которая читает файл, по одной строке за раз, и вызывает мою функцию «findNonNumericFields» в каждой строке, чтобы собрать все столбцы, которые в конечном итоге должны быть удалены. Пару красно-зеленых циклов, и я закончил, снова имея рабочую функцию и полный тест.
Теперь я решил, что мне следует провести рефакторинг. Так как мой метод "findNonNumericFields" был разработан только потому, что я решил, что он понадобится при реализации "collectNonNumericColumns", мне кажется, что было бы разумно, чтобы "findNonNumericFields" стал закрытым. Однако это сломало бы мои первые тесты, поскольку у них больше не было бы доступа к методу, который они тестировали.
Итак, я получаю приватные методы и набор тестов, которые его проверяют. Так как многие люди советуют не проверять частные методы, я чувствую, что я загнал себя в угол. Но где именно я потерпел неудачу?
Я полагаю, что я мог бы начать с более высокого уровня, написав тест, который проверяет то, что в конечном итоге станет моим публичным методом (то есть findAndFilterOutAllNonNumericColumns), но это несколько противоречит цели TDD (по крайней мере, согласно дяде Бобу) : Вы должны постоянно переключаться между написанием тестов и рабочим кодом, и что в любой момент времени все ваши тесты работали в последнюю минуту или около того. Потому что, если я начну с написания теста для общедоступного метода, у меня будет несколько минут (или часов, или даже дней в очень сложных случаях), прежде чем я получу все детали в частных методах для работы, чтобы тестирование общедоступного метода метод проходит.
Так что делать? Является ли TDD (с быстрым циклом красный-зеленый-рефактор) просто несовместимым с частными методами? Или есть ошибка в моем дизайне?
источник
private
будто это имеет смысл сделать так.Ответы:
Единицы
Я думаю, что могу точно определить, где проблема началась:
За этим следует немедленно спросить себя: «Будет ли это отдельный тестируемый модуль
gatherNonNumericColumns
или его часть?»Если ответ « да, отдельный », то ваш порядок действий прост: этот метод должен быть общедоступным в соответствующем классе, чтобы его можно было проверить как единое целое. Ваш менталитет что-то вроде: «Мне нужно протестировать один метод, и мне также нужно проверить другой метод»
Из того, что вы говорите, вы поняли, что ответ « нет, часть того же ». На этом этапе ваш план больше не должен полностью писать и тестировать, а
findNonNumericFields
затем писатьgatherNonNumericColumns
. Вместо этого следует просто написатьgatherNonNumericColumns
. На данный момент, этоfindNonNumericFields
должно быть лишь вероятной частью пункта назначения, который вы имели в виду, выбирая следующий красный контрольный пример и проводя рефакторинг. На этот раз ваш менталитет звучит так: «Мне нужно протестировать один метод, и пока я делаю это, я должен помнить, что моя законченная реализация, вероятно, будет включать этот другой метод».Держать короткий цикл
Выполнение вышеизложенного не должно привести к проблемам, которые вы описываете в своем предпоследнем абзаце:
Ни в коем случае этот метод не требует от вас написания красного теста, который станет зеленым только тогда, когда вы реализуете все
findNonNumericFields
с нуля. Гораздо более вероятно,findNonNumericFields
что этот код начнётся в виде встроенного кода в открытом методе, который вы тестируете, который будет создаваться в течение нескольких циклов и в конечном итоге извлечен во время рефакторинга.Дорожная карта
Чтобы дать примерную дорожную карту для этого конкретного примера, я не знаю точных тестовых случаев, которые вы использовали, но скажу, что вы писали в
gatherNonNumericColumns
качестве публичного метода. Тогда, скорее всего, тестовые случаи будут такими же, как те, для которых вы написалиfindNonNumericFields
, каждый из которых использует таблицу только с одной строкой. Когда этот однострочный сценарий был полностью реализован, и вы хотели написать тест, чтобы вынудить вас извлечь метод, вы бы написали двухстрочный случай, который потребовал бы, чтобы вы добавили свою итерацию.источник
Многие люди думают, что модульное тестирование основано на методах; это не. Это должно быть основано вокруг самой маленькой единицы, которая имеет смысл. Для большинства вещей это означает, что класс - это то, что вы должны тестировать как единое целое. Не индивидуальные методы на это.
Теперь, очевидно, вы будете вызывать методы класса, но вы должны думать о тестах как о применении к имеющемуся у вас объекту черного ящика, чтобы вы могли видеть, какие логические операции предоставляет ваш класс; это то, что вам нужно проверить. Если ваш класс настолько велик, что логическая операция слишком сложна, тогда у вас есть проблема разработки, которая должна быть сначала исправлена.
Класс с тысячей методов может показаться тестируемым, но если вы тестируете только каждый метод по отдельности, вы на самом деле не тестируете класс. Некоторым классам может потребоваться находиться в определенном состоянии перед вызовом метода, например, сетевой класс, которому необходимо установить соединение перед отправкой данных. Метод отправки данных не может рассматриваться независимо от всего класса.
Таким образом, вы должны увидеть, что частные методы не имеют отношения к тестированию. Если вы не можете использовать свои закрытые методы, вызывая открытый интерфейс вашего класса, тогда эти закрытые методы бесполезны и не будут использоваться в любом случае.
Я думаю, что многие люди пытаются превратить частные методы в тестируемые модули, потому что кажется, что для них легко запускать тесты, но это слишком детализирует тестирование. Мартин Фаулер говорит
что имеет большой смысл для объектно-ориентированной системы, объекты которой разрабатываются как единицы. Если вы хотите протестировать отдельные методы, возможно, вам следует создать процедурную систему, например C, или класс, состоящий полностью из статических функций.
источник
Тот факт, что ваши методы сбора данных достаточно сложны, чтобы заслуживать тестирования и достаточно отделены от вашей основной цели, чтобы быть их собственными методами, а не частью какого-то цикла, указывают на решение: сделайте эти методы не частными, а членами какого-то другого класса это обеспечивает сбор / фильтрацию / табулирование функциональности.
Затем вы пишете тесты для глупых аспектов обработки данных вспомогательного класса (например, «различение чисел от символов») в одном месте, а тесты для вашей основной цели (например, «получение данных о продажах») в другом месте, и вы надеваете не нужно повторять базовые тесты фильтрации в тестах для вашей нормальной бизнес-логики.
В целом, если ваш класс, выполняющий одну задачу, содержит обширный код для выполнения другой задачи, которая требуется, но не связана с его основным назначением, этот код должен находиться в другом классе и вызываться через открытые методы. Он не должен быть скрыт в закрытых углах класса, который только случайно содержит этот код. Это улучшает тестируемость и понятность одновременно.
источник
Лично я чувствую, что когда вы писали тесты, вы глубоко ушли в сознание реализации. Вы предполагали, что вам понадобятся определенные методы. Но вам действительно нужно, чтобы они делали то, что должен делать класс? Удастся ли классу, если кто-нибудь придет и проведет их рефакторинг? Если бы вы использовали класс (и это, по моему мнению, должно быть мышление тестера), вам было бы действительно все равно, если есть явный метод проверки чисел.
Вы должны протестировать открытый интерфейс класса. Частная реализация является частной по причине. Это не часть публичного интерфейса, потому что он не нужен и может меняться. Это деталь реализации.
Если вы пишете тесты для открытого интерфейса, вы никогда не столкнетесь с проблемой, с которой столкнулись. Либо вы можете создавать тестовые примеры для открытого интерфейса, которые охватывают ваши частные методы (отлично), либо вы не можете. В этом случае, возможно, пришло время подумать о частных методах и, возможно, полностью отказаться от них, если они все равно не могут быть достигнуты.
источник
Вы не делаете TDD, основываясь на том, что класс ожидает от вас внутри.
Ваши тесты должны быть основаны на том, что класс / функциональность / программа имеет отношение к внешнему миру. В вашем примере будет ли пользователь когда-либо вызывать ваш класс читателя с
find all the non-numerical fields in a line?
Если ответ «нет», то это плохой тест, чтобы писать в первую очередь. Вы хотите написать тест на функциональность на уровне класса / интерфейса, а не на уровне «что должен реализовать метод класса, чтобы заставить это работать», а именно на том, что представляет собой ваш тест.
Поток TDD это:
Это НЕ нужно делать, «потому что в будущем мне понадобится X как частный метод, позвольте мне реализовать его и сначала протестировать». Если вы обнаружите, что делаете это, вы делаете «красный» этап неправильно. Это, кажется, ваша проблема здесь.
Если вы часто пишете тесты для методов, которые становятся частными, вы делаете одну из следующих вещей:
источник
Вы сталкиваетесь с распространенным заблуждением при тестировании в целом.
Большинство новичков в тестировании начинают думать так:
и так далее.
Проблема здесь в том, что у вас фактически нет модульного теста для функции H. Тест, который должен проверять H, фактически проверяет H, G и F одновременно.
Чтобы решить эту проблему, вы должны понимать, что тестируемые модули никогда не должны зависеть друг от друга, а скорее от их интерфейсов . В вашем случае, когда модули являются простыми функциями, интерфейсы являются просто их сигнатурой вызова. Поэтому вы должны реализовать G таким образом, чтобы его можно было использовать с любой функцией, имеющей ту же сигнатуру, что и F.
Как именно это можно сделать, зависит от вашего языка программирования. Во многих языках вы можете передавать функции (или указатели на них) в качестве аргументов другим функциям. Это позволит вам протестировать каждую функцию отдельно.
источник
Предполагается, что тесты, которые вы пишете во время разработки через тестирование, гарантируют, что класс правильно реализует свой открытый API, и в то же время обеспечивают легкость тестирования и использования этого открытого API.
Вы можете во что бы то ни стало использовать частные методы для реализации этого API, но нет необходимости создавать тесты через TDD - функциональность будет проверена, потому что публичный API будет работать правильно.
Теперь предположим, что ваши закрытые методы достаточно сложны, чтобы они заслуживали отдельных тестов, но они не имеют смысла как часть открытого API вашего исходного класса. Что ж, это, вероятно, означает, что на самом деле они должны быть публичными методами в каком-то другом классе, который ваш исходный класс использует в своей собственной реализации.
Только тестируя общедоступный API, вы значительно облегчаете изменение деталей реализации в будущем. Бесполезные тесты будут раздражать вас позже, когда их нужно переписать, чтобы поддержать какой-то элегантный рефакторинг, который вы только что обнаружили.
источник
Я думаю, что правильный ответ - это вывод, к которому вы пришли, начиная с публичных методов. Вы начнете с написания теста, который вызывает этот метод. Это не получится, поэтому вы создадите метод с таким именем, который ничего не делает. Тогда вы, возможно, правы тест, который проверяет возвращаемое значение.
(Мне не совсем понятно, что делает ваша функция. Возвращает ли она строку с содержимым файла с удалением нечисловых значений?)
Если ваш метод возвращает строку, вы проверяете это возвращаемое значение. Таким образом, вы просто продолжаете создавать его.
Я думаю, что все, что происходит в приватном методе, должно быть в публичном методе в какой-то момент во время вашего процесса, а затем перемещаться только в приватный метод как часть шага рефакторинга. Насколько мне известно, рефакторинг не требует провальных тестов. Вам нужно только проваливать тесты при добавлении функциональности. Вам просто нужно запустить тесты после рефакторинга, чтобы убедиться, что все они пройдены.
источник
Там старая поговорка.
Люди, кажется, думают, что когда вы используете TDD, вы просто садитесь, пишете тесты, и дизайн просто волшебным образом происходит. Это не правда Вам нужно иметь план высокого уровня. Я обнаружил, что я получаю наилучшие результаты от TDD, когда я сначала проектирую интерфейс (публичный API). Лично я создаю фактическое,
interface
которое определяет класс первым.задыхаясь, я написал какой-то "код", прежде чем писать какие-либо тесты! Ну нет. Я не Я написал контракт, которому нужно следовать, дизайн . Я подозреваю, что вы могли бы получить аналогичные результаты, набросав UML-диаграмму на миллиметровой бумаге. Дело в том, что у вас должен быть план. TDD не является лицензией на то, чтобы взломать кусок кода.
Я действительно чувствую, что «Test First» - это неправильное название. Дизайн Во- первых , то испытание.
Конечно, пожалуйста, следуйте советам других по извлечению большего количества классов из вашего кода. Если вы сильно чувствуете необходимость проверить внутренние компоненты класса, извлеките эти внутренние компоненты в легко тестируемый модуль и введите его.
источник
Помните, что тесты тоже могут быть реорганизованы! Если вы делаете метод закрытым, вы сокращаете общедоступный API, и, следовательно, вполне приемлемо отбросить некоторые соответствующие тесты для этой «потерянной функциональности» (AKA уменьшил сложность).
Другие говорили, что ваш приватный метод будет вызываться как часть ваших других тестов API, или он будет недоступен и, следовательно, может быть удален. На самом деле, все обстоит более детально, если мы думаем о путях выполнения .
Например, если у нас есть публичный метод, который выполняет деление, мы можем захотеть проверить путь, который приводит к делению на ноль. Если мы сделаем метод закрытым, мы получим выбор: либо мы можем рассмотреть путь деления на ноль, либо мы можем исключить этот путь, учитывая, как он вызывается другими методами.
Таким образом, мы можем отбросить некоторые тесты (например, делить на ноль) и реорганизовать другие с точки зрения оставшегося публичного API. Конечно, в идеальном мире существующие тесты заботятся обо всех этих оставшихся путях, но реальность всегда компромисс;)
источник
Бывают случаи, когда закрытый метод можно сделать общедоступным методом другого класса.
Например, у вас могут быть частные методы, которые не являются поточно-ориентированными и оставляют класс во временном состоянии. Эти методы могут быть перемещены в отдельный класс, который хранится в частном порядке вашим первым классом. Поэтому, если ваш класс является очередью, у вас может быть класс InternalQueue, который имеет открытые методы, а класс Queue хранит экземпляр InternalQueue конфиденциально. Это позволяет вам проверить внутреннюю очередь, а также прояснить, что представляют собой отдельные операции в InternalQueue.
(Это наиболее очевидно, когда вы представляете, что класса List нет, и если вы пытались реализовать функции List как частные методы в классе, который их использует.)
источник
Интересно, почему ваш язык имеет только два уровня конфиденциальности, полностью публичный и полностью приватный.
Можете ли вы сделать так, чтобы ваши непубличные методы были доступны для пакетов или что-то в этом роде? Затем поместите свои тесты в один и тот же пакет и наслаждайтесь тестированием внутренней работы, которая не является частью общедоступного интерфейса. Ваша система сборки исключит тесты при сборке бинарного релиза.
Конечно, иногда вам нужны действительно приватные методы, не доступные ни для кого, кроме определяющего класса. Я надеюсь, что все такие методы очень малы. В целом, поддержание небольших методов (например, ниже 20 строк) очень помогает: тестирование, сопровождение и простое понимание кода становятся проще.
источник
Иногда я прибегал к частным методам для защиты, чтобы обеспечить более детальное тестирование (более жесткое, чем у открытого публичного API). Это должно быть (надеюсь, очень редкое) исключение, а не правило, но оно может быть полезно в определенных конкретных случаях, с которыми вы можете столкнуться. Кроме того, это то, что вы не хотели бы учитывать при создании общедоступного API, больше «обмана», который можно использовать для внутреннего использования в таких редких ситуациях.
источник
Я испытал это и почувствовал вашу боль.
Мое решение было:
прекратить относиться к испытаниям, как к строительству монолита.
Помните, что когда вы написали набор тестов, скажем, 5, чтобы исключить некоторые функциональные возможности, вам не нужно держать все эти тесты рядом , особенно когда это становится частью чего-то другого.
Например, я часто имею:
так что у меня есть
Однако, если я сейчас добавлю функции (функции) более высокого уровня, которые ее вызывают, которые имеют много тестов, я мог бы теперь сократить эти тесты низкого уровня до следующих:
Дьявол кроется в деталях, и способность делать это будет зависеть от обстоятельств.
источник
Солнце вращается вокруг земли или земля вокруг солнца? По мнению Эйнштейна, ответ «да» или обе, поскольку обе модели отличаются только с точки зрения, аналогично, инкапсуляция и разработка, основанная на тестировании, конфликтуют только потому, что мы думаем, что это так. Мы сидим здесь, как Галилей и папа, обижаясь друг на друга: дурак, разве ты не видишь, что частные методы тоже нуждаются в проверке; еретик, не нарушай инкапсуляцию! Аналогично, когда мы признаем, что истина грандиознее, чем мы думали, мы можем попробовать что-то вроде инкапсуляции тестов для частных интерфейсов, чтобы тесты для открытых интерфейсов не нарушали инкапсуляцию.
Попробуйте это: добавьте два метода, один из которых не имеет входных данных, но justs возвращает количество закрытых тестов, а другой принимает число тестов в качестве параметра и возвращает pass / fail.
источник