Комментарии 29
Буду рад любым примерам того, как четкое следование данному принципу приводило к проблемам и снижению мотивации писать тесты у команды разработчиков.
Т.е. вы не только противоречите общепринятым нормам, но ещё и просите у общественности помощи в свержении норм???
Есть многое, что вам можно ответить на этот призыв… Но, пожалуй, хватит одного: «чёткое следование принципам» нередко оказывается итальянской забастовкой :)
Иначе говоря, цель должна определять необходимый инструментарий. Если цель у вас неверная — то и традиционный инструмент будет казаться кривым.
А касательно именно тестов — у вас проблема в другом месте. Вы имеете
И это если не говорить о том, что бы у вас был TDD, описанная проблема была бы совсем невозможна…
Да, легаси без тестов несовместимо с TDD. Даже просто unit-тесты прикрутить — надо постараться, но оно того порой стоит. А если не стоит — не надо ругать TDDЮ, надо выбирать подходящий инструмент — например интеграционные автотесты.
Т.е. вы не только противоречите общепринятым нормам, но ещё и просите у общественности помощи в свержении норм???
Я говорю, что принцип хороший, но при чрезмерно педантном отношении к нему, может приводить к проблемам.
В случае с legacy-кодом есть проблема с написанием юнит-тестов, связанная с конфигурацией окружения. Я согласен, что полезно иметь такой тест, в котором будет понятно, как, собственно, использовать этот метод. При создании такого теста получается, своего рода, и описание работы модуля.
Но когда у вас есть 10 таких тестов, и 5 из них падает с NullRef exception, в том месте, где не должно этого было быть, вам остается только сидеть и дебажить сами тесты. Это в корне, на мой взгляд, не соответствует идее TDD, согласно которой тесты должны быть простыми и ясными.
Я говорю, что принцип хороший, но при чрезмерно педантном отношении к нему, может приводить к проблемам.Чрезмерное отношение всегда плохо, везде.
Как я уже написал, TDD и legacy не совместимы в стартовой позиции, ибо в TDD тесты бы уже были. Их нет — у вас другая проблема. Для её решения есть другие методологии. Если кратко — рефакторинг вас спасёт.
Чтобы такого не происходило надо писать тесты к тестам
При тестировании приватных методов, тестируются не контракты, а конкретная реализация.
При изменении внутренней структуры класса(например пару методов удалили, другую пару объединили) не будет никакой возможности узнать не нарушились ли внешние контракты, так как тесты все равно упадут, и все придется внимательно проверять руками.
А проблема с копипастом может решаться хэлпером для создания минимальной дефолтной конфигурации.
Хелперы — это палка с двумя концами. Один помогает, другой бьет по башке. Когда у вас есть лес классов из хелперов и непонятно, какой в вашем случае подходит, вы просто напишете новый. В итоге где-то в тестах что-то начинает валиться и вы тратите время на долгий и нужный дебаг тестов. Что опять, на мой взгляд, не соответвует идее TDD.
Но я не говорю тут об интеграционных тестах.
непонятна цель написания тестов в таком случае. При измении кода они не помогут вам найти ошибку.
>Хелперы — это палка с двумя концами. Один помогает, другой бьет по башке.
Проблема не в хелперах, а в — «Когда у вас есть лес классов из хелперов и непонятно, какой в вашем случае подходит, вы просто напишете новый.»
Возможно вам стоит начать с наведения порядка в проекте. Думаю что много аспектов станет проще, не только тестирование.
То, что вы описываете с хелперами, это как написание еще одного продакшн кода. Потом понадоятся тесты для хелперов, чтобы проверить, что они правильно работают.
1. У вас получается что качество кода «используемых им других классов.» обеспечивается тестами написанными у тех кто от этих классов зависит. Это достаточно странная логика.
2. На автономные классы без зависимостей по такой логике тесты можно не писать.
При нормальном подходе к тестам, зависимости в свою очередь должны быть покрыты тестами, так что дополнительные проверки им не нужны.
- есть метод, не являющийся методом интерфейса класса, но выполняющий нетривиальную логику.
- покрываешь его тестами так, чтобы быть уверенным в его 100% работе в известных случаях.
- в публичном методе есть логика вызова этого метода, которую ты покрываешь (и только)
Знаете технику Sprout Method? Это оно и есть.
Ваш основной public-метод генерирует множество входящих значений параметров для непубличного метода. Вы проверятете не каждое значение, а то, что это множество правильно передается в непубличный метод. А непубличный метод вы проверяете тпо всему множеству передаваемых значений (по каждому важному). В итоге вам не надо писать 8 тестов для непубличного метода и 8 тестов для публичного. Для публично, в зависимости от логики, может оказаться достаточным, например, только 2.
есть метод, не являющийся методом интерфейса класса, но выполняющий нетривиальную логику. покрываешь его тестами так, чтобы быть уверенным в его 100% работе в известных случаях.А если этот метод сделать публичным или превратить в прокси к публичному классу/методу — то и заморочки с непубличностью пропадут. У вас снова в тестах будут только публичные методы :)
Кстати, по вашей ссылке «Sprout Method» как раз про это.
Иначе говоря — не надо тестировать приватное. Надо сделать публичным и тогда тестировать.
Если у вас полностью покрыт публичный метод, и у вас есть правило покрывать все публичные методы. Непонятно зачем вообще в таком случае проверять приватный.
Теоретически, если вам понадобилось тестировать "промежуточную точку", дёргая приватный метод — было бы неплохо разрезать класс по этой промежуточной точке, тестируя публичные интерфейсы обрезков.
Разработчик должен писать тесты не потому что этого хочет другой разработчик/менеджер, а потому что это надо разработчику. Все сы люди, все мы человеки. Мы пока ещё часто ошибаемся и у нас не идеальная память. Если мне через пол года надо будет править код, то тесты мне помогут с примерами использования и с большой долей вероятности защитят от ошибки.
ИМХО, считаю что не надо заставлять писать тесты, а надо тбъяснять зачем это и показывать пример.
И да, в моём понимании юнит тесты должны быть максимально глупыми. (Обычно в тестах забываю про наследования, дженерики… и тупо максимально всё забиваю ручками)
Этот метод внутри выполняет очень много операций и использует другие сервисы, которые ему дают информацию о праздниках, чаевых и пр.
А корень проблемы-то — вот. Вы сделали переусложненный метод, и вместо того, чтобы среагировать на переусложненные тесты как на симптом сложного кода и разобраться с кодом, ищете обходные пути.
Данный метод требует 10 проверок, чтобы покрыть все случаи, 8 из них существенные. [...] При написании юнит-тестов для проверки этой логики, разработчику придется написать не меньше 8 больших юнит-тестов.
Это еще почему?
Я сталкивался со случаями, когда конфигурация юнит-теста занимала 50+ строк кода, при 4 строках непосредственного вызова. Т.е. только, примерно, 10% кода несёт полезную нагрузку.
Простите, а что в тестировании этого метода — конфигурация, а что — полезная нагрузка?
А корень проблемы-то — вот. Вы сделали переусложненный метод, и вместо того, чтобы среагировать на переусложненные тесты как на симптом сложного кода и разобраться с кодом, ищете обходные пути
Ноп. Проблема в том, что код, как положено, разбит на простые методы и есть метод, который всё собирает. Этот метод публичный. Простые методы — приватные. Итоговая логика объективно сложная и чтобы протестировать отдельные части, реализуемые отдельными приватными методами надо изрядно извернуться. Это не проблема кода, это именно проблема и ограничение юнит-тестирования. Согласно канону, конечно можно всё растащить по утилитным классам, но это усложнение программы только ради следования канонической теореме тестирования. По-моему, это как раз то место, где следование канону несёт один сплошной вред и никакой пользы.
Вообще, лично мне кажется разумным было бы ввести в язык специальную аннотацию, помечающую метод как, вроде бы приватный, но, в то же время, открытый для тестирования. Пока для таких случаев некоторые используют пустую аннотацию, вроде @TheReasonTheMethodISPublikIsOnlyForTesting :). Костыль, конечно, но что делать. Рефлексия — ещё более кривой костыль.
Обычно это говорит ещё и том, что извернуться надо будет, чтобы и просто штатно использовать этот публичный метод.
Это не проблема кода, это именно проблема и ограничение юнит-тестирования.
Неа. Юнит-тестирование вам не запрещает это делать (более того, в каком-то смысле — стимулирует, потому что приватный метод — тоже юнит). Природа рекомендации "тестируйте публичный интерфейс" в другом — этот подход требует меньше переписывания при рефакторинге. Внутреннее разбиение метода на приватные методы — детали его реализации, и может меняться раз в десять минут. Каждый раз тесты переписывать? А откуда брать критерии на каждый метод?
публичный класс DbBackupper с одним простым методом CreateBackup(string path), который делает резервную копию БД.Это плохой пример. Чтобы проверить бэкап нужен или инфраструктурный или интеграционный тест.
Более реалистичный пример — метод, который данные из БД тащит. У него под капотом и подключение к БД, и инициализация команды/запроса, и трансформация данных, и может что-то ещё. При этом полезная нагрузка — это инициализация параметров запроса/команды и трансформация данных. Правильный подход для покрытия тестами — отсечь всю инфраструктурщину, переложив её на кастомный (если надо) адаптер, оставив в изначальном методе вызов трёх методов: PrepareQuery, GetData, TransformData. GetData тестируется интеграционно, а в модульных тестах заменяется на заглушку. Два других метода имеют вполне понятные контракты, которые покрываются тестами. Более того, при таком рефакторинга запросто можно вытащить кучу однотипных методов, которые сольются в один… и станет в коде чище :)
Да очень просто быть: у интернал классов тоже есть "публичный" интерфейс, просто он "публичен" в рамках библиотеки. Дальше методика решения зависит от среды, в .net это решается InternalsVisibleTo
.
А этот "единственный публичный класс с одним простым методом" нельзя протестировать юнит-тестом, если у вас нет контроля за внутренностями библиотеки, только интеграционными (и это тоже надо делать, отдельно от юнит-тестов).
2) Лично я всегда следую принципу с минимальными усилиями получить максимальный результат. Исходя из этого считаю допустимым в тесте через рефлексию установить значение приватного поля. Но это скорее исключение чем общее правило.
Тестировать только через public-методы плохо