Это не руководство, какие символы нужно ввести в редакторе кода, чтобы получились модульные тесты. Это — пища для ума, которую необходимо употребить до того, как предпринимать упомянутые действия.
Тема модульного тестирования не так проста, как может показаться. Многие из нас, разработчиков, приходят в модульное тестирование под давлением клиентов, сотрудников, коллег, своих кумиров и так далее. Мы быстро понимаем его ценность, и, закончив технические приготовления, забываем об общей картине, если вообще когда-либо её понимали. В этой статье я вкратце расскажу о том, чем является и чем не является модульное тестирование как в целом, так и в PHP, а заодно опишу, какое место занимает модульное тестирование в сфере QA.
Что такое тестирование?
Прежде чем углубляться в модульные тесты, нужно изучить теорию самого тестирования, чтобы не делать ошибок вроде той, что совершили авторы одного из самых популярных PHP-фреймворков: на своём сайте они показали интеграционные тесты и назвали их модульными. Нет, Laravel, это не модульные тесты. Хотя это не мешает мне всё ещё любить этот фреймворк.
Тестирование ПО определяется как «расследование, проведённое с целью предоставления заинтересованным сторонам информации о качестве продукта». Этому противопоставляется «тестирование ПО — это пустая трата бюджета проекта разработчиками, которые не делают ничего важного, а затем просят ещё времени и денег, потому что «ничего» может быть весьма дорогим». Тут ничего нового.
Вот моя краткая история становления тестирования:
- 1822 — Разностная машина (Difference engine) (Чарльз Бэббидж).
- 1843 — Аналитическая машина (Analytical engine) (Ада Лавлейс).
- 1878 — Эдисон вводит термин «баг».
- 1957 — Тестирование и отладка программ (Чарльз Бэйкер).
- 1958 — Первая команда тестирования ПО (Джеральд Вайнберг).
- 1968 — Кризис ПО (Фридрих Бауэр).
- 1970-е — Модель «водопад», реляционная модель, декомпозиция, критический анализ (Walkthrough), проектирование и инспектирование кода, качество и метрики, шаблоны проектирования.
- 1980-е — CRUD-анализ, архитектура системы, автотестирование, V-модель, надёжность, стоимость качества, способы использования, шаблоны ООП-проектирования.
- 1990-е — Scrum, usability-тестирование, MoSCoW, эвристическое тестирование, автоматизация ПО и тестирования.
Если вы относитесь к поколению миллениалов, как я, то, возможно, будете поражены, что команды тестировщиков существовали ЗАДОЛГО до вашего рождения. Остановитесь на минутку, вдохните, выдохните, успокойтесь.
История показывает, как в течение времени менялся тип тестирования, которое считалось «достаточно хорошим» для заинтересованных сторон. Примерные фазы, на что ориентировались при тестировании:
- … — 1956 отладка
- 1957 — 1978 демонстрация
- 1979 — 1982 разрушение (destruction)
- 1983 — 1987 оценка
- 1988 — … предотвращение
Следовательно, модульное тестирование необходимо для предотвращения несоответствий между проектом и реализацией.
Чем на самом деле является тестирование?
Есть разные классификации тестирования ПО. Чтобы лучше понимать место модульного тестирования, упомяну лишь о наиболее широкораспространённых подходах.
Тесты бывают: статические и динамические, «ящичные» (белый ящик, чёрный ящик, серый ящик), уровни и типы. В рамках каждого подхода используются разные критерии классификации.
Статическое и динамическое тестирование
Статическое тестирование проводится без исполнения кода. Сюда относится корректура, проверка, ревизия кода (при наблюдении за работой другого / парном программировании), критический анализ, инспекции и так далее.
Динамическое тестирование для получения корректных результатов требует исполнять код. Например, для модульных тестов, интеграционных, системных, приёмочных и прочих тестов. То есть тестирование проводится с использованием динамических данных, входных и выходных.
«Ящичный» подход
Согласно этому подходу, все тесты ПО делятся на три вида ящиков:
- Тестирование типа «белый ящик» проверяет внутренние структуры и модули, игнорирует ожидаемую функциональность для конечных пользователей. Это может быть тестирование API, внесение неисправностей (fault injection), модульное тестирование, интеграционное тестирование.
- Тестирование типа «чёрный ящик» больше интересуется тем, что делает ПО, а не как делает. Это означает, что тестировщики не обязаны ни разбираться в объекте тестирования, ни понимать, как он работает под капотом. Такой тип тестирования нацелен на конечных пользователей, их опыт взаимодействия с видимым интерфейсом. К «чёрным ящикам» относится тестирование на основе моделей, тестирование способов использования, таблицы переходов состояний, спецификационное тестирование и т. д.
- Тестирование типа «серый ящик» проектируется со знанием программных алгоритмов и структур данных (белый ящик), но выполняется на пользовательском уровне (чёрный ящик). Сюда относится регрессионное тестирование и шаблонное тестирование (pattern testing).
Теперь, чтобы запутать вас, скажу, что модульное тестирование может относиться и к «чёрному ящику», поскольку вы можете разбираться в тестируемом модуле, но не во всей системе. Хотя для меня оно по-прежнему «белый ящик», и предлагаю вам с этим согласиться.
Уровни тестирования
Их количество варьируется, обычно, в диапазоне от 4 до 6, и все они полезны. Названия тоже бывают разные, в зависимости от принятой в компании культуры вы можете знать «интеграционные» тесты как «функциональные», «системные» тесты как «автоматизированные», и так далее. Для простоты я опишу 5 уровней:
- Модульное тестирование.
- Интеграционное тестирование.
- Тестирование интерфейсов компонентов.
- Системное тестирование.
- Эксплуатационное приёмочное тестирование.
Модульное тестирование проверяет функциональность конкретного куска кода, обычно по одной функции за раз. Интеграционное тестирование проверяет интерфейсы между компонентами, чтобы собранные воедино модули формировали систему, работающую, как задумано. Это важный момент, потому что большое количество тестов, которые называют модульными, на самом деле являются интеграционными тестами, а разработчики считают их модулями. Если подразумевается использование нескольких модулей — это тестирование интеграции между ними, а не самих модулей. Тестирование интерфейсов компонентов проверяет данные, передаваемые между разными модулями. Например, получили данные из модуля 1 — проверили — передали в модуль 2 — проверили. Системное тестирование — это сквозное тестирование ради проверки соблюдения всех требований. Эксплуатационное приёмочное тестирование выполняется для проверки готовности к эксплуатации. Оно не является функциональным, проверяется лишь работоспособность сервисов, не повреждают ли какие-то подсистемы среду и прочие сервисы.
Типы тестирования
Каждый тип тестирования, вне зависимости от его уровня, также может подразделяться на другие типы. Существует больше 20 общепринятых типов. Самые распространённые:
- Регрессионное тестирование.
- Приёмочное тестирование.
- Дымовое (smoke) тестирование.
- UAT
- Разрушительное (Destructive) тестирование.
- Тестирование производительности.
- Непрерывное тестирование.
- Usability-тестирование.
- Тестирование безопасности.
Из названия понятно, для чего предназначен тот или иной тип тестирования. Жирным выделены модульные тесты в PHP. Если очень хочется, то к модульному тестированию можно применить каждый из этих терминов. Однако главной разновидностью модульных тестов являются тесты регрессионные, которые проверяют, все ли модули системы исполняются корректно после внесения изменений в код.
Теперь вы знаете, что модульные тесты являются динамическими, относятся к классу «белый ящик», выполняются на уровне модулей, представляют собой регрессионные тесты, но при этом под модульными тестами можно понимать многие разновидности тестов. Так что же такое на самом деле модульные тесты?
Что такое модульное тестирование?
V-модель — это графическое представление вышеупомянутых уровней, типов и их назначения в жизненном цикле разработки ПО.
После проверки и утверждения подробных требований к продукту, когда уже начали писать код, первой линией защиты от любых несоответствий становятся модульные тесты. Поэтому компании, понимающие, что они делают, заставляют разработчиков использовать модульные тесты или даже TDD, поскольку гораздо дешевле исправить баги на начальных этапах, чем на более поздних.
И это справедливо. У модульных тестов масса достоинств. Они:
- Изолируют каждую часть программы и проверяют её корректность.
- Помогают рано обнаруживать проблемы.
- Заставляют разработчиков мыслить в рамках входных, выходных и ошибочных условий.
- Придают коду удобный для тестирования вид, облегчают будущий рефакторинг.
- Упрощают интегрирование рабочих (!) модулей.
- Частично заменяют техническую документацию.
- Заставляют отделять интерфейс от реализации.
- Доказывают, что код модуля работает так, как ожидалось (хотя бы математически).
- Могут использоваться как низкоуровневые наборы регрессионных тестов.
- Демонстрируют прогресс в незавершённой системной интеграции.
- Снижают стоимость исправления багов (с TDD — ещё больше).
- Позволяют улучшать архитектуру приложения с помощью определения ответственности модулей.
- Если вы можете это протестировать, то можете присоединить к своей системе.
- Модульное тестирование — это ВЕСЕЛО!
Однако, есть определённые ограничения, о которых вы подумали, вероятно, при чтении этого списка:
- Модульное тестирование не вылавливает ошибки интегрирования.
- Каждое булево выражение требует как минимум двух тестов, и количество быстро растёт.
- Модульные тесты столь же глючные, как и тестируемый ими код.
- Привязка тестов к паре конкретных фреймворков или библиотек может ограничить рабочий процесс.
- Большинство тестов пишется после завершения разработки. Печально. Используйте TDD!
- Возможно, после маленького рефакторинга система будет работать как прежде, но тесты будут сбоить.
- Вырастает стоимость разработки.
- Человеческая ошибка: комментирование сломанных тестов.
- Человеческая ошибка: добавление в код обходных путей специально для прохождения модульных тестов.
Последнее убивает меня больше всего. (Почти) в каждом проекте прямо в исходном коде рабочего приложения я нахожу строки наподобие «если это модульный тест, грузить суррогатную SQLite базу данных, в противном случае грузить другую БД», или «если это модульный тест, не отправлять письмо, в противном случае отправлять», и так далее. Если у вашего приложения плохая архитектура, не притворяйтесь, что можете исправить паршивое ПО с помощью хорошего прохождения тестов, оно от этого не станет лучше.
Я часто обсуждал с коллегами и клиентами, что такое хороший модульный тест. Он:
- Быстрый.
- Автоматизированный.
- Полностью управляет всеми своими зависимостями.
- Надёжен: может запускаться в любом порядке, вне зависимости от других тестов.
- Может запускаться только в памяти (никаких взаимодействий с БД, чтений/записей в файловой системе).
- Всегда возвращает один результат.
- Удобен для чтения и сопровождения.
- Не тестирует SUT-конфигурацию (system under test).
- Имеет чётко определённую ЕДИНСТВЕННУЮ ЗАДАЧУ.
- Хорошо именован (и достаточно понятно, чтобы избежать отладки только ради выяснения, что же сбоит).
Тем, кто ухмыльнулся, прочитав «автоматизированный»: я не имел в виду интегрирование PHPUnit или JUnit в CI-конвейеры. Речь идёт о том, что если вы меняете код, сохраняете его и не знаете, проходят ли модули свои тесты, то они не автоматизированы, а должны бы. Выигрышный вариант — отслеживание файлов (File watcher).
Что нужно подвергать модульному тестированию?
В нормальных системах модульные тесты нужно писать для:
- Модулей — неделимых изолированных частей системы, которые выполняют какую-то одну задачу (функция, метод, класс).
- Публичных методов.
- Защищённых методов, но только в редких случаях и когда никто не видит.
- Багов и их исправлений.
Определение модульного теста зависит от разработчика, написавшего код. В PHP это почти всегда метод класса или функция, потому что это неделимая часть ПО, имеющая смысл сама по себе. Несколько раз я видел, как разработчики в качестве одного модуля использовали массив из однометодных миниклассов. Это имеет смысл, если минимальная функциональность требует наличия нескольких объектов.
Так что вы сами можете определять, что для вас является модулем. Или можете тестировать методы один за другим, упростив жизнь тому парню, что потом будет работать с кодом.
Если вы не проводите модульное тестирование, предлагаю заняться этим после возникновения следующего большого бага. Проверьте, с каким методом он будет связан, напишите сбойный тест с правильными аргументами и результатом, исправьте баг, снова запустите модульный тест. Если он будет пройден, то можете быть уверены, что этот баг пришлось исправлять в последний раз (с учётом ваших определённых входных сценариев).
Такой подход помогает легче понять модульное тестирование. Проанализируйте отдельно каждый метод. Поставщики данных могут помочь определить входные и выходные данные для любых сценариев, которые могут прийти вам в голову, поэтому что бы ни произошло, вы будете знать, чего ожидать.
Что НЕ нужно тестировать
Чуть сложнее определить, что тестировать не нужно. Я постарался собрать список элементов, которые не нужно подвергать модульному тестированию:
- Функциональность за пределами контекста (scope) модулей (!)
- Интеграция модулей с другими модулями (!)
- Неизолированное поведение (неимитируемые (unmockable) зависимости, настоящие БД, сеть)
- Приватные, защищённые методы.
- Статичные методы.
- Внешние библиотеки.
- Ваш фреймворк.
Уверен, не следует применять модульное тестирование ни к чему из вышеперечисленного, кроме статичных методов. Мне нравится аргументировать, что статичность, по сути, означает процедуральность, причём в многих случаях процедуральность глобальную. Если статичный метод вызывает другой статичный метод, то эту зависимость нельзя переопределить. А это значит, что вы теперь тестируете не изолированно. И тогда это уже не модульное тестирование. С другой стороны, это же часть кода, которая может жить сама по себе, у неё есть предназначение, и её нужно тестировать, чтобы удостовериться: какую бы часть этой бестолковой системы ни вызвала тестируемая часть кода, та не сломается. Поэтому считаю, что тестировать статичные методы можно, если вы уверены, что выходные данные вашего теста не сможет изменить никакой другой тест, и что язык или фреймворк позволят тестировать нативно.
Как писать модульные тесты?
- Пишите код, пригодный для модульного тестирования, затем тестируйте его.
- Пишите код, пригодный для модульного тестирования, затем тестируйте его.
- Пишите код, пригодный для модульного тестирования, затем тестируйте его.
Если «затем тестируйте его» недостаточно, то на laracasts.com есть очень хорошие видео про модульное тестирование PHP. Есть и масса сайтов, посвящённых той же задаче в других языках. Не вижу смысла объяснять, как я выполняю модульное тестирование, потому что инструменты меняются довольно быстро, и когда вы прочитаете этот текст, я могу переключиться с PHPUnit на Kahlan. Или нет. Кто знает.
Но ответить на первый вопрос (как писать код, пригодный для модульного тестирования) гораздо легче, и вряд ли ситуация сильно изменится со временем:
- SOLID
- DRY
- Отсутствие новых ключевых слов в конструкторе.
- Отсутствие циклов в конструкторе (и переходов, если это оговаривается).
- Отсутствие статичных методов, параметров, классов.
- Отсутствие методов setup(): объекты должны быть полностью инициализированы после конструирования.
- Отсутствие синглтонов (глобального состояния) и прочих нетестируемых антипаттернов.
- Отсутствие всемогущих объектов (God objects).
- Отсутствие классов со смешанной функциональностью (mixed concern classes).
- Отсутствие скрытых зависимостей.
Теперь, зная, чем являются и чем не являются модульные тесты, что нужно и что не нужно тестировать, какое место занимают модульные тесты в жизненном цикле разработки ПО, вам будет легче реализовывать их. Осталось найти фреймворк или библиотеку по душе. Если сомневаетесь, берите фреймворк/язык, ставший стандартом де-факто.
В заключение: модульные тесты очень важны как для разработчиков, так и для бизнеса. Их нужно писать, существуют отработанные методики, которые помогут вам легко покрыть модули тестами, в основном с помощью подготовки самих модулей. Но все эти методики не имеют смысла без знания теории тестирования, описанной в этой статье. Нужно уметь отличать модульные тесты от тестов других типов. И когда у вас в голове будет ясное понимание, то и писать тесты вам станет гораздо легче.
Автор: Макс