о юнит-тестах (программ.)
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-11 10:50 am (UTC)Основная проблемма с unit-тестированием - временные затраты на их изготовление (тестов), на их поддержку и, в особенности, на их изменение при рефакторинге. Добавить к этому также то, что использование классов и шаблонов проектирования (design patterns) многим не нравится из-за "размазывания" логики по тысяче классов и файлов. Рассуждения всегда стопорятся на констатации диалектического единства и борьбы противоположностей (больше тестов - низкая производительность, меньше тестов - хуже качество). В этот момент у всех срабатывает предохранитель - можно авторитетно заявить о необходимости иметь чувство меры и загадочно удалиться из спора. Мало кому хочется копать дальше. Гораздо проще заявить, что надо тестировать, но не слишком много.
А никаких конкретных критериев избыточности тестов (или избыточности тестирования в целом) не приводить.
В нашей голове любой специфический алгоритм может быть сколько угодно сложной и уникальной идеей. Но стоит попробовать переформулировать его, чтобы изложить на бумаге так, чтобы все поняли - исходные понятия и сущности, в которых разум оперировал, обязательно вдруг трансформируются в набор из стандартных понятий. Каждое из этих стандартных понятий будет каким-то универсальным алгоритмом, оператором и т.д.. В программировании развитие языков идет, как мне кажется по пути переноса как можно большей части логики в декларативную сферу. Из уникального - в универсальное. В конце концов любая программа в идеале должна выглядеть как маленькая процедурка, в которой сосредоточена вся уникальность алгоритма. Соответственно, закулисами, компилятор и процессор могут в реальности производить сложнейшие вычисления. Но такой дизайн кода не будет казаться "размазанной логикой".
Так должно быть в идеале, и не всегда легко добиться этого на практике. Не могу сказать, что даже C++ идеально подходит для этого. Для универсализации кода зачастую надо либо templates использовать, либо множество дополнительных заплаток приписывать вроде деструкторов, функторов и т.п.. Добавить к этому то, что отлаживать все эти заплатки проблематично, заставить всех разработчиков (в том числе и 3rd party) использовать и декларировать исключения - невозможно физически. Становится понятно почему такой мощный и, казалось бы, легко абстрагируемый язык, тем не менее ведет к монстрообразным программам.
Основной аргумент "за TDD" - подготовка тестов заставляет разработчика не писать код, потребующий значительного рефакторинга в будущем. Тесты до написания кода превращают любой сколь угодно сложный алгоритм в относительно простое взаимодействие модулей с декларативной фунциональностью. Постоянный рефакторинг - это попытка добиться максимального перевода логики из процедурной в декларативную со стремлением выкристаллизовать ядро уникальности. Стандартные же модули однажды протестированные, не должны требовать рефакторинга. На практике многие, хоть и дробят программу на классы, но делают это не по принципу экстракции универсального, а по принципу укорачивания кода процедуры - кстати, я помню, как мне в университете говорили, что больше 40 строк в процедуре быть не должно быть. Или более современно - для минимизации каких-нибудь метрик (например, не больше 4-5 условных операторов в процедуре).
Успешное использование TDD возможно только при определенной организации разработки. Когда проводятся постоянные code review, introspective meetings, pair programming, когда вся компания находится в постоянном состоянии познания, если хотите. В отсутствие всего этого TDD действительно ведет к неоправданным затратам, тупому формализму, дурацким багам и т.д. и т.п.. Это - лекарство как часть общего курса лечения под наблюдением врача, а по-отдельности - всего лишь сильнодействующее токсичное вещество.