Модульное тестирование сегодня применяется самым активным образом для практически всех языков программирования, за очень редким исключением. И на это есть свои причины. Давайте поговорим сейчас о модульном тестировании более подробно.
Введение
В позапрошлом номере "Компьютерных вестей" мы с вами уже достаточно подробно обсудили разные виды тестов, которым подвергаются приложения, перед тем как станут пригодны для "выхода в люди". Не буду повторять содержание той статьи - если кто-то захочет, её очень просто найти на сайте нашей газеты. Так вот, среди прочих видов тестов там упоминалось и модульное тестирование, которое по-английски называется unit testing, ну и по-русски, соответственно, пишется и говорится нередко именно как unit-тестирование.
Идея, лежащая в основе модульного тестирования, заключается в отдельном тестировании поведения каждой самостоятельной единицы программного кода. Поскольку сейчас программный код в подавляющем большинстве пишется на объектно-ориентированных языках, то в качестве "кирпичика" кода выступает написанный программистом класс, а если переходить на ещё более глубокий уровень, то даже не класс, а отдельно взятый метод этого класса.
Unit-тесты для своего кода пишут сами программисты. Одним из преимуществ модульного тестирования является возможность выявления ошибок на ранних этапах разработки самим разработчиком. Это, конечно, несколько увеличивает нагрузку на разработчика, но позволяет здорово сэкономить время на отладке готового кода приложения, а также даёт ещё целый ряд преимуществ, которые мы обсудим подробнее чуть ниже. Нужно сказать, что "писать unit-тесты" в наше продвинутое в технологическом плане время вовсе не означает писать их совершенно самостоятельно, что называется, "с нуля". Потому что существует масса специализированных библиотек и фреймворков для тестирования, позволяющих если не на все 100%, то по крайней мере очень и очень значительно автоматизировать создание модульных тестов для приложений, которые пишутся на самых разных языках программирования. Об этих фреймворках - не обо всех, конечно, а о самых известных, потому как всех в одной статье ну никак не упомянуть, - мы ещё немного поговорим чуть дальше.
Как это выглядит на практике?
Сам по себе тест - это какая-то функция/метод, сначала вызывающая другие функции/методы, которые и нужно протестировать, а затем сравнивающая возвращаемые ими значения с эталоном. Обычно в рамках одного теста моделируется широкий спектр входных данных для тестируемой функции - чтобы быть уверенным, что она будет вести себя как положено при любых значениях входных параметров. Эту функцию потом вызывают в рамках автоматического "прогона" тестов по коду.
Итак, как же выглядит применение unit-тестирования на практике? В зависимости от выбранной методологии разработки программного обеспечения применение unit-тестов в, так сказать, производственном процессе может выглядеть по-разному. Интереснее всего с точки зрения применения модульных тестов выглядит подход, который называется "разработкой через тестирование" (test-driven development). Особенность данного подхода состоит в том, что тесты при этом пишутся ещё до написания основного кода. Да-да, согласен, это выглядит достаточно необычно, если вы ни разу не сталкивались ни с чем подобным ранее, однако если подумать, то всё не так и странно, как кажется поначалу.
Дело в том, что до написания самого программного кода, который нужно тестировать, тесты, конечно же, работать не будут. Но никто и не говорит, что они должны работать до этого - задача написания тестов до создания тестируемого кода заключается в конкретизации требований к тестируемым классам и методам. После того, как разработаны тесты и написан "каркас" кода, по которому эти тесты уже можно "прогонять", стоит приступать к написанию самого кода. При этом задача программиста состоит в том, чтобы написать такой код, который успешно "продерётся" через тернии всевозможных тестов и будет при этом отвечать заранее разработанным спецификациям.
Хотя в написании тестов ещё до разработки кода нет технологически обусловленной необходимости, многие рассматривают именно такой порядок работы с тестами как одно из необходимых условий их успешного применения. Дело в том, что каждый unit-тест удорожает стоимость разработки (ведь, как минимум, несколько её удлиняет). Таким образом, чтобы тест был написан не зря (и время не его написание не ушло впустую), нужно, чтобы это тест хотя бы раз не был выполнен успешно. Если разработчик пишет тесты ещё до основного кода, то шанс, что тест "завалится", намного выше, чем когда тест разрабатывается после самого кода, и разработчик уже знает, как именно может его код отреагировать на те или иные моменты, а потому и немного "похимичить" с тестом, чтобы он был правильным всегда - то есть неэффективным.
Разработка с таким подходом как бы усложняется и становится длиннее, что в ряде случаев может не понравиться заказчику. Но при этом убиваются некоторые дополнительные зайцы, что уже выглядит как плюс для того программиста или для той конторы, в которой применяется test-driven development. Давайте посмотрим вместе, что даёт использование этой методики разработки программ.
Плюсы и минусы unit-тестирования
Как и всё в нашем несовершенном мире, модульное тестирование имеет не только плюсы, но и минусы. Но сначала давайте всё-таки о плюсах, потому что иначе, прочитав о минусах, многие просто бросят эту статью и не узнают о том, чем unit-тесты полезны.
Во-первых, модульное тестирование позволяет быть уверенным, что из-за исправления одного бага у вас не появился другой. Поскольку тесты работают в автоматическом режиме, вы всегда можете отследить описанную выше ситуацию (проведя, тем самым, регрессионное тестирование). Ещё один плюс, который, по большому счёту, группируется с первым, - unit-тесты позволяют безболезненно проводить рефакторинг кода (то есть изменение самого кода без какого бы то ни было изменения его функциональности). Многие даже утверждают, что модульные тесты поощряют рефакторинг кода разработчиками.
Второй плюс заключается в том, что unit-тесты служат некоторым образом документацией к коду и путеводителем по нему. И вправду, тесты могут рассказать, как та или иная функции должны реагировать на разные входные параметры - описание этого поведения зачастую по не вполне объяснимым причинам (все понимают, что основная причина - лень и работа в режиме "это нужно было реализовать ещё вчера") отсутствует в комментариях, оставляемых программистами.
Третий плюс состоит в том, что применение unit-тестов даёт более качественное отделение интерфейса от реализации. Поскольку в рамках одного теста проверяется один класс, то все "порочные" связи с другими классами, не предусмотренные архитектурой приложения, всплывают на поверхность и безжалостным образом разрываются.
Теперь давайте и о минусах. Первые минус - вполне логичное продолжение последнего плюса. Если вы тестируете каждый элемент программного кода в отдельности от других, никто не сможет сказать наверняка, что произойдёт, когда вы всю эту кучу-малу смешаете воедино. То есть, пардон, не смешаете, а интегрируете в единое целое. Поэтому не обойтись без дополнительных интеграционных тестов.
Но это ещё полбеды. Следующий минус заключается в том, что при недостаточно продуманной архитектуре проекта тесты могут мешать изменению кода, а не способствовать ему. Ситуация не такая уж и редкая, а потому и далеко не последняя в списке неприятностей.
Но хуже всего то, что даже тесты, успешно выполненные и перевыполненные после внесения всяческих изменений, не дают никакой гарантии, что в коде нет ошибок. И дело здесь даже не в подгонке тестов под код, а в пресловутом человеческом факторе. Иногда несколько изменив значения входных параметров в рамках тестов программист может, сам того не желая, пропустить ошибку в тестируемом коде или, хуже того, внести её туда. Но, увы и ах, от ошибок, связанных с человеческим фактором, не застрахован никто и никогда.
Инструменты
Выше я упоминал, что есть специальные инструменты, которые помогают программисту писать unit-тесты, и даже пообещал упомянуть некоторые из них. Что ж, выполняю это обещание.
Названия большинства популярных фреймворков для unit-тестирования образованы от названия языка, на котором пишется программный код, и слова "unit". Например, для Java это JUnit, для .NET'овских языков - NUnit, для Delphi - DUnit, для C++ - CPPUnit, для PHP - PHPUnit, для Python - PyUnit, для ActionScript - AsUnit... Ну и так далее. А если вы не найдёте фреймворка для unit-тестирования к тому языку, на котором вы пишете... Что ж, скорее всего, этот язык или ещё не придумали, или уже нигде не используют, или это Ассемблер.
Резюме
Что ж, как видите, в самой идее unit-тестирования нет ничего сложного. Да и в практике, пожалуй, тоже. Нужно только найти где-то время и силы на изучение тестировочного фреймворка для используемого вами языка программирования и для того, чтобы писать сами тесты. А время, особенно время программиста, - ресурс, как известно, весьма и весьма дефицитный. Впрочем, возможно, из прочитанного выше вы почерпнули, что время, потраченное на unit-тесты, того стоит. Остаётся только пожелать вам успехов в их освоении и применении.
Вадим СТАНКЕВИЧ,
dreamdrusch@tut.by
Горячие темы