о юнит-тестах (программ.)
Oct. 9th, 2007 10:18 pmЯ отношусь с некоторым подозрением к созданию юнит-тестов, покрывающих весь объем исходного кода, в таких языках, как C++ и Java.
В целом мне нравится идея TDD (test-driven development), и мне не раз случалось убеждаться в пользе большого числа быстрых тестов. Но вместе с тем, у меня есть несколько возражений, особенно в случае C++/Java.
Во-первых, если я правильно понимаю историю (тут я повторяю то, что читал где-то), движение TDD зародилось в среде динамических языков. В языках с статическими проверками типизации значительная часть того, что проверяют юнит-тесты в динамических языках, проверяется "бесплатно" компилятором (естественно, это не значит, что динамические языки хуже - у динамической типизации есть свои преимущества). Это, казалось бы, хорошо, но на практике нередко оказывается, что юнит-тесты на C++/Java выходят почти тривиальными, за счет того, что из типичного для динамического языка юнит-теста убирают проверки типизации. В принципе эта проблема решается, нужно просто научиться писать более подходящие для этих языков юнит-тесты, в которых достаточно "мяса".
Во-вторых, слишком много ресурсов и времени уходит на подготовку кода к тому, чтобы его оттестировать (как правило, значительно больше, чем в динамических языках, потому что в них вставлять и втискиваться внутрь существующего кода обычно намного легче). Всевозможные mock'и, разные схемы dependency injection, и прочая мишура, нужная для того, чтобы в конце концов гордо запустить юнит-тест, изолирующий сложный класс, симулирующий с помощью разного рода методов все его заимодействие с внешней средой, и в итоге почти тривиализирующий его поведение, так что тест проходит и все замечательно, да? - нет, потом окажется, что баги мигрировали в область интеракции класса с его окружением.
Я не согласен с довольно распостраненной точкой зрения, гласящей, что вся эта мишура - mock'и, дополнительные интерфейсы/абстрактные классы, позволяющие тестам легко втиснуться в иерархию, и проч. и проч. - что все это полезно само по себе, а не только потому, что помогает тестировать. Что дескать это очень правильно, мы упрощаем и абстрагируем код для удобства проверок, и он от этого только улучшается, поэтому время, на это потраченное, себя окупает. Мне это кажется довольно малооправданным примером wishful thinking. Иногда так действительно бывает; но очень часто бывает и так, что навороченные внутри и снаружи кода бутафорные пристройки на самом деле не проясняют код, не улучшают его, и ни для чего, кроме как запуска тестов, не пригодятся. Я такое видел много раз. Можно провести определенную параллель с design pattern'ами. На них тоже у многих людей едут мозги, и они начинают творить кучи паттернов там, где можно было сделать многое проще, и гордиться самим фактом использования паттернов.
Вместе с тем я все же, несмотря на эти подозрения, принимаю практику массивного юнит-тестирования кода на C++ и Java. Преимущества - обычно - перевешивают вышеупомянутые недостатки. Просто я считаю, что не стоит доходить в этом до фанатизма (и это не пустое заявление - примеры такого фанатизма, по крайней мере по моему мнению, я нередко вижу вокруг себя), и стоит понимать, что цена, которую мы платим за адаптацию нашего кода в C++/Java к максимальному юнит-тестированию, часто бывает весьма высокой, выше, чем кажется на первый взгляд.
В целом мне нравится идея TDD (test-driven development), и мне не раз случалось убеждаться в пользе большого числа быстрых тестов. Но вместе с тем, у меня есть несколько возражений, особенно в случае C++/Java.
Во-первых, если я правильно понимаю историю (тут я повторяю то, что читал где-то), движение TDD зародилось в среде динамических языков. В языках с статическими проверками типизации значительная часть того, что проверяют юнит-тесты в динамических языках, проверяется "бесплатно" компилятором (естественно, это не значит, что динамические языки хуже - у динамической типизации есть свои преимущества). Это, казалось бы, хорошо, но на практике нередко оказывается, что юнит-тесты на C++/Java выходят почти тривиальными, за счет того, что из типичного для динамического языка юнит-теста убирают проверки типизации. В принципе эта проблема решается, нужно просто научиться писать более подходящие для этих языков юнит-тесты, в которых достаточно "мяса".
Во-вторых, слишком много ресурсов и времени уходит на подготовку кода к тому, чтобы его оттестировать (как правило, значительно больше, чем в динамических языках, потому что в них вставлять и втискиваться внутрь существующего кода обычно намного легче). Всевозможные mock'и, разные схемы dependency injection, и прочая мишура, нужная для того, чтобы в конце концов гордо запустить юнит-тест, изолирующий сложный класс, симулирующий с помощью разного рода методов все его заимодействие с внешней средой, и в итоге почти тривиализирующий его поведение, так что тест проходит и все замечательно, да? - нет, потом окажется, что баги мигрировали в область интеракции класса с его окружением.
Я не согласен с довольно распостраненной точкой зрения, гласящей, что вся эта мишура - mock'и, дополнительные интерфейсы/абстрактные классы, позволяющие тестам легко втиснуться в иерархию, и проч. и проч. - что все это полезно само по себе, а не только потому, что помогает тестировать. Что дескать это очень правильно, мы упрощаем и абстрагируем код для удобства проверок, и он от этого только улучшается, поэтому время, на это потраченное, себя окупает. Мне это кажется довольно малооправданным примером wishful thinking. Иногда так действительно бывает; но очень часто бывает и так, что навороченные внутри и снаружи кода бутафорные пристройки на самом деле не проясняют код, не улучшают его, и ни для чего, кроме как запуска тестов, не пригодятся. Я такое видел много раз. Можно провести определенную параллель с design pattern'ами. На них тоже у многих людей едут мозги, и они начинают творить кучи паттернов там, где можно было сделать многое проще, и гордиться самим фактом использования паттернов.
Вместе с тем я все же, несмотря на эти подозрения, принимаю практику массивного юнит-тестирования кода на C++ и Java. Преимущества - обычно - перевешивают вышеупомянутые недостатки. Просто я считаю, что не стоит доходить в этом до фанатизма (и это не пустое заявление - примеры такого фанатизма, по крайней мере по моему мнению, я нередко вижу вокруг себя), и стоит понимать, что цена, которую мы платим за адаптацию нашего кода в C++/Java к максимальному юнит-тестированию, часто бывает весьма высокой, выше, чем кажется на первый взгляд.
no subject
Date: 2007-10-09 10:24 pm (UTC)TDD -- это когда я пишу что-то вроде:
assertEquals(fact(1), 1);
assertEquals(fact(3), fact(2)*3);
assertRaises(fact(-1));
и пишу я это *до* того, как придумаю реализацию fact(). Мало ли как можно вычислять факториал :) Но его *свойства* я в тестах хотя бы отчасти зафиксирую (хотя желательно полностью).
Увы, в случае БД дело сильно осложняется.
no subject
Date: 2007-10-09 10:29 pm (UTC)Кстати да. TDD это обманка такая для хакеров. Чтобы как-то заставить их думать о требованиях перед тем как писать код.
no subject
Date: 2007-10-09 10:36 pm (UTC)Просто TDD многими воспринимается как логическое завершение все более увеличивающегося внимания к тестам :) Собственно, в этом взгляде на TDD есть резон.
no subject
Date: 2007-10-09 10:50 pm (UTC)В одном из проектов в котором я работал старые юнитесты, не выдержав трудностей рефакторинга были полностью убраны из процесса билда. На их месте начали возникать новые где-то через два года.
no subject
Date: 2007-10-09 11:15 pm (UTC)no subject
Date: 2007-10-09 11:22 pm (UTC)добавлю лишь, что непомерное разрастание мишуры mock'ов и прочие втискивание юнит-тестов в состояние сферического коня в вакууме сигнализирует о том, что в архитектуре что-то не так.. (ну, это очень близко к тому что "если для ваших юнит-тестов требуются юнит-тетсирование...")
ещё начал замечать, что там где обоснованно выбран ++/Java в качестве языка реализации и используется это всё "на полную", обычно кораздо актуальнее integration testing, нежели хорошее покрытие юнит-тестами, ибо изолировать юниты для тестинга действительно иногда довольно накладно и зачастую не оправдывает целей.
no subject
Date: 2007-10-09 11:23 pm (UTC)no subject
Date: 2007-10-10 12:54 am (UTC)Соглашусь с несколько измененным утверждением: "Я отношусь с некоторым подозрением к вере в то, что юнит-тесты, покрывающих весь объем исходного кода, в таких языках, как C++ и Java, дают некоторые гарантии его корректного исполнения".
Или с таким: "Я не считаю разумным тратить дополнительные ресурсы на покрытие юнит-тестами всего объема исходного кода".
С исходным же утверждением я не могу согласиться, ибо постоянно убеждаюсь, что написание юнит-теста к свежеобнаруженной ошибке часто повышает процент покрытия тестами объема исходного кода.
То есть тенденция все же налицо: чем выше процент покрытия, тем ниже вероятность сбоя.
Другое дело, что повышать процент покрытия любой ценой обычно неэффективно. И есть разные рекомендации по тому, как увеличить шансы добраться до необнаруженной ошибки: например, тестировать только публичные методы. Но это уже другой разговор.
no subject
Date: 2007-10-10 03:18 am (UTC)Любое перетряхивание исходников, независимо от цели, часто полезно само по себе :)
no subject
Date: 2007-10-10 04:46 am (UTC)no subject
Date: 2007-10-10 05:35 am (UTC)По-идее, юнит должен быть достаточно самостоятельной частью кода с чёттко выделенным и задокументированным интерфейсом. Тогда место mock'ов занимает "обвязка", которая просто проверяем выходы юнита на заданных входах без вмешательства в его структуру. И такое определение юнитов действительно улучшает общую читаемость...
no subject
Date: 2007-10-10 06:04 am (UTC)Но сейчас я попал в компанию где тестирования не проводится даже минимального. Где человек выкладывает код, а его работоспособность пусть проверяют тестеры, "иначе за что им деньги платят". Фактически другая половина мира, те, кто не приемлют unit-тестыв как класс (пока, надеюсь).
Не скажу, что появилась куча свободного времени для творчества, отчасти потому что основная работа - править (чужие) баги :(
no subject
Date: 2007-10-10 06:17 am (UTC)1) необходимость их написания заставляет держать мух отдельно, котлеты отдельно, а не замешивать их в один класс;
2) ими гораздо проще тестировать нештатные ситуации, особенно при работе со сторонними библиотеками и именно эти тесты увеличивают покрытие.
Возможно люди которые упирают на стопроцентное покрытие забывают про необходимость smoke-тестов регрессионных тестов и т.д.
Что же касается оверхеда связанного с дополнительными интерфейсами и т.д. очень помогает хороший рефакторинг туль. Но к сожалению для ++ я такого не видел, хотя очень хочется.
no subject
Date: 2007-10-10 07:09 am (UTC)Упомянутые две категории тестов - сильно разные. Проблема встраивания первых - это проблема хорошей инкапсуляции логики, и в этом смысле из наличие, безусловно, улучшают архитектуру.
Проблема встраивания high level юнит-тестов - это частный случай проблемы конфигурируемости. Если у вас сложно сделать high level юнит-тест, значит, вашу систему и вообще сложно сколько-нибудь нетривиально сконфигурировать. Насколько это плохо - очевидно, зависит от системы. Часто ничего плохого в этом нет, но все-таки data-driven системы обычно удобнее разрабатывать коллективно.
no subject
Date: 2007-10-10 07:31 am (UTC)Поскольку значительная часть ошибок (в Java, я так понимаю, больше половины) - интеграционные, тестировать приходится не только отдельные "юниты", но также и обособленные сущности иерархии. Вот тут и начинается возня с конфигурированием, о которой говорит avva.
no subject
Date: 2007-10-10 07:31 am (UTC)no subject
Date: 2007-10-10 07:42 am (UTC)Т.е. мне кажется, что слово unit по-хорошему должно означать не обособленную единицу кода, а обособленную часть требований.
Потому что неправильно (по-моему) тестировать адекватность средства достижения цели, явным образом используя при этом структуру этого же средства.
no subject
Date: 2007-10-10 07:47 am (UTC)Был код Х. Мы добавили к нему тесты и получили пару (X, tests). Затем (или сразу в процессе добавления) мы провели рефакторинг пары (Х, tests) и пришли к состоянию (X', tests'). После этой операции, согласно сторонникам ТДД, мы увидим, что Х' оказался лучше и красивее, чем Х, даже если отбросить тесты.
Не то чтобы я с этим до конца согласен, но разумные и работающие способы использования этой идеи действительно существуют.
no subject
Date: 2007-10-10 07:56 am (UTC)В теории это правильно, но для разработки на Java или С++ по-моему очень неудобно как минимум в силу ограниченности их синтаксиса (плохо хотя бы то, что код, банально, не компилируется долгое время; не говоря о более тонких вещах вроде того, что абстракцию high order function можно выразить десятью разными способами, и какой тут окажется правильным, априори совершенно непонятно).
TDD отлично работает там, где задача описывается на DSL, или даже непосредственно данными. Идеальный пример - компиляторы :)
no subject
Date: 2007-10-10 08:31 am (UTC)no subject
Date: 2007-10-10 08:41 am (UTC)http://avva.livejournal.com/1818941.html?thread=45227069#t45227069
Если принять такую терминологию, то идея 100% покрытия кода именно юнит-тестами мне кажется крайне нелепой.
Вообще, интеграционные тесты в моем коде встречаются чаще и оказываются на порядок полезнее, хотя бы просто потому что намного чаще ломаются.
no subject
Date: 2007-10-10 09:35 am (UTC)no subject
Date: 2007-10-10 09:48 am (UTC)no subject
Date: 2007-10-10 12:04 pm (UTC)no subject
Date: 2007-10-10 12:55 pm (UTC)А из голой идеи unit тестирования вырастают замечательные практики в коллективе.
Например, это требование коммитить код только вместе с unit test'ами. Тогда репозиторий становится чище и стабильнее. Или, скажем, ночная сборка и автоматический запуск всех юнитов и acceptance'ов - это как пульс в коллективе.
Что касается mock'ов, то тут нужна гармония. Я не думаю что от mockup классов код становится лучше. Я полностью согласен с вами, что такие mock'и приводят обычно к тривилизации теста и его бесмысленности. Кроме того, в таком тесте потом, спустя полгода, разобраться нереально, а поддерживать - pain in the ass. Здесь я сторонник довольно больших unit test'ов, где проверяется не один конкретный класс, а целая система.
no subject
Date: 2007-10-10 01:09 pm (UTC)Да, именно это и есть самый большой недостаток юнит тестов - ложное чувство уверенности и удобная, но совершенно неверная, метрика качества
no subject
Date: 2007-10-10 01:35 pm (UTC)no subject
Date: 2007-10-10 01:41 pm (UTC)И так бывает...
Date: 2007-10-10 02:22 pm (UTC)no subject
Date: 2007-10-10 02:38 pm (UTC)no subject
Date: 2007-10-10 06:11 pm (UTC)На Ваш взгляд такой подход имеет право на существование или нет?
no subject
Date: 2007-10-10 07:16 pm (UTC)no subject
Date: 2007-10-10 10:05 pm (UTC)засланную на конференцию из России
где автор использовал теорию категорий для слздания теорий тестов и рисовал много таких диаграм
было несколько забавно
related reading
Date: 2007-10-11 05:30 am (UTC)http://beust.com/weblog/archives/000462.html
http://beust.com/weblog/archives/000463.html
especially coming from a testing person, but always pragmatic!
no subject
Date: 2007-10-11 08:23 am (UTC)Я не очень понимаю, как на таком уровне абстракции отразить методики тестирования, сколько-нибудь приближенные к реальности. Тестирование ведь довольно мало напоминает, скажем, формальное доказательство свойств программы.
no subject
Date: 2007-10-11 10:50 am (UTC)Основная проблемма с unit-тестированием - временные затраты на их изготовление (тестов), на их поддержку и, в особенности, на их изменение при рефакторинге. Добавить к этому также то, что использование классов и шаблонов проектирования (design patterns) многим не нравится из-за "размазывания" логики по тысяче классов и файлов. Рассуждения всегда стопорятся на констатации диалектического единства и борьбы противоположностей (больше тестов - низкая производительность, меньше тестов - хуже качество). В этот момент у всех срабатывает предохранитель - можно авторитетно заявить о необходимости иметь чувство меры и загадочно удалиться из спора. Мало кому хочется копать дальше. Гораздо проще заявить, что надо тестировать, но не слишком много.
А никаких конкретных критериев избыточности тестов (или избыточности тестирования в целом) не приводить.
В нашей голове любой специфический алгоритм может быть сколько угодно сложной и уникальной идеей. Но стоит попробовать переформулировать его, чтобы изложить на бумаге так, чтобы все поняли - исходные понятия и сущности, в которых разум оперировал, обязательно вдруг трансформируются в набор из стандартных понятий. Каждое из этих стандартных понятий будет каким-то универсальным алгоритмом, оператором и т.д.. В программировании развитие языков идет, как мне кажется по пути переноса как можно большей части логики в декларативную сферу. Из уникального - в универсальное. В конце концов любая программа в идеале должна выглядеть как маленькая процедурка, в которой сосредоточена вся уникальность алгоритма. Соответственно, закулисами, компилятор и процессор могут в реальности производить сложнейшие вычисления. Но такой дизайн кода не будет казаться "размазанной логикой".
Так должно быть в идеале, и не всегда легко добиться этого на практике. Не могу сказать, что даже C++ идеально подходит для этого. Для универсализации кода зачастую надо либо templates использовать, либо множество дополнительных заплаток приписывать вроде деструкторов, функторов и т.п.. Добавить к этому то, что отлаживать все эти заплатки проблематично, заставить всех разработчиков (в том числе и 3rd party) использовать и декларировать исключения - невозможно физически. Становится понятно почему такой мощный и, казалось бы, легко абстрагируемый язык, тем не менее ведет к монстрообразным программам.
Основной аргумент "за TDD" - подготовка тестов заставляет разработчика не писать код, потребующий значительного рефакторинга в будущем. Тесты до написания кода превращают любой сколь угодно сложный алгоритм в относительно простое взаимодействие модулей с декларативной фунциональностью. Постоянный рефакторинг - это попытка добиться максимального перевода логики из процедурной в декларативную со стремлением выкристаллизовать ядро уникальности. Стандартные же модули однажды протестированные, не должны требовать рефакторинга. На практике многие, хоть и дробят программу на классы, но делают это не по принципу экстракции универсального, а по принципу укорачивания кода процедуры - кстати, я помню, как мне в университете говорили, что больше 40 строк в процедуре быть не должно быть. Или более современно - для минимизации каких-нибудь метрик (например, не больше 4-5 условных операторов в процедуре).
Успешное использование TDD возможно только при определенной организации разработки. Когда проводятся постоянные code review, introspective meetings, pair programming, когда вся компания находится в постоянном состоянии познания, если хотите. В отсутствие всего этого TDD действительно ведет к неоправданным затратам, тупому формализму, дурацким багам и т.д. и т.п.. Это - лекарство как часть общего курса лечения под наблюдением врача, а по-отдельности - всего лишь сильнодействующее токсичное вещество.
no subject
Date: 2007-10-11 07:59 pm (UTC)no subject
Date: 2007-10-14 06:27 pm (UTC)no subject
Date: 2007-10-16 03:53 pm (UTC)no subject
Date: 2007-10-16 04:08 pm (UTC)Я не могу, увы, подробно обсуждать, как организована система тестирования внутри Гугля :)
no subject
Date: 2007-10-16 05:02 pm (UTC)