Как писать unit тесты php
PHP Profi
Квест → Как хакнуть форму
PHPUnit для начинающих. Часть 1: Начните использование. Перевод Серия
Это первая часть серии «PHPUnit для начинающих». В этом руководстве мы объясним для чего покрывать код unit-тестами и всю мощь инструмента PHPUnit. В конце мы напишем простой тест с использованием PHPUnit.
Типы тестов
Прежде чем мы погрузимся в PHPUnit давайте разберём различные типы тестов. В зависимости от того, как вы хотите категоризировать их, в PHPUnit применяются любые типы тестов для разработки ПО.
Давайте разделим тесты на категории по уровню их специфичности. По данным Википедии. В целом существует 4 признанных уровня тестов:
Если мы поместим типы тестов в пирамиду, выглядеть это будет так:
Что такое PHPUnit
Глядя на пирамиду вверху, мы можем сказать, что юнит-тестирование — это строительный материал для всех остальных видов тестирования. Когда мы закладываем сильную базу, мы способны построить устойчивое приложение. Однако написание тестов вручную, а также запуск тестов каждый раз когда вы вносите изменения — это процесс трудоёмкий. Если бы существовал инструмент для автоматизации этого процесса, то написание тестов стало бы гораздо более приятным занятием.
Вот где выходит на сцену PHPUnit. В настоящее время PHPUnit наиболее популярный фреймворк для юнит-тестирования в PHP. Кроме наличия таких возможностей, как моки (подделки) объектов, он также может анализировать покрытие кода, логирование и предоставляет тысячи других возможностей.
Давайте установим PHPUnit в нашей системе:
Если вы работаете на Unix-подобной системе, то это вы можете сделать следующими командами:
Если вы сделали всё верно, то вы сможете увидеть версию установленного PHPUnit, набрав в вашем терминале команду:
Ваш первый unit-тест
Теперь создайте файл для тестов «CalculatorTest.php» и скопируйте в него следующий код. Мы остановимся на каждом методе более детально.
Заключительной частью проделанной работы является запуск PHPUnit и проверка, что все тесты проходят (выполняются без ошибок). В вашем терминале зайдите в директорию где вы создали файл с тестами и запустите следующую команду:
Если вы всё сделали правильно, то вы должны увидеть что-то вроде этого:
Заключение
Мы завершили первое руководство из серии «PHPUnit для начинающих». В следующей статье мы собираемся показать вам как использовать Data Provider (поставщик данных) в ваших тестах.
Надеемся это простое руководство поможет вам в вашей разработке и поможет начать использовать unit-тестирование.
Если вам понравился перевод на эту тему читайте нас в Twitter, подписывайтесь на наши группы в Facebook, ВКонтакте, Google+. Мы будем рады продолжить серию.
Unit-тесты, пытаемся писать правильно, чтобы потом не было мучительно больно
100 модульных тестов и приходится тратить продолжительное время на то чтобы заставить их работать вновь. Итак, приступим к «хорошим рекомендациям» при написании автоматических модульных тестов. Нет, я не буду капитаном очевидностью, в очередной раз описывая популярный стиль написания тестов под названием AAA (Arange-Act-Assert). Зато попытаюсь объяснить, чем отличается Mock от Stub-а и что далеко не все тестовые объекты — «моки».
Глобально модульные тесты можно условно поделить на две группы: тесты состояния (state based) и тесты взаимодействия (interaction tests).
Тесты состояния — тесты, проверяющие что вызываемый метод объекта отработал корректно, проверяя состояние тестируемого объекта после вызова метода.
Тесты взаимодействия — это тесты, в которых тестируемый объект производит манипуляции с другими объектами. Применяются, когда требуется удостовериться, что тестируемый объект корректно взаимодействует с другими объектами.
Стоит также заметить, что модульный (unit) тест может запросто превратиться в интеграционный тест, если при тестировании используется реальное окружение(внешние зависимости) — такие как база данных, файловая система и т.д.
Интеграционные тесты — это тесты, проверяющие работоспособность двух или более модулей системы, но в совокупности — то есть нескольких объектов как единого блока.
В тестах взаимодействия же тестируется конкретный, определенный объект и то, как именно он взаимодействует с внешними зависимостями.
Внешняя зависимость — это объект, с которым взаимодействует код и над которым нет прямого контроля. Для ликвидации внешних зависимостей в модульных тестах используются тестовые объекты, например такие как stubs (заглушки).
Стоит заметить что существует классический труд по модульным тестам за авторством Жерарда Месароша под названием «xUnit test patterns: refactoring test code«, в котором автор вводит аж 5 видов тестовых объектов, которые могут запросто запутать неподготовленного человека:
— dummy object, который обычно передается в тестируемый класс в качестве параметра, но не имеет поведения, с ним ничего не происходит, никакие методы не вызываются. Примером таких dummy-объектов являются new object(), null, «Ignored String» и т.д.
— test stub (заглушка), используется для получения данных из внешней зависимости, подменяя её. При этом игнорирует все данные, могущие поступать из тестируемого объекта в stub. Один из самых популярных видов тестовых объектов. Тестируемый объект использует чтение из конфигурационного файла? Передаем ему ConfigFileStub возвращающий тестовые строки конфигурации для избавления зависимости на файловую систему.
— test spy (тестовый шпион), используется для тестов взаимодействия, основной функцией является запись данных и вызовов, поступающих из тестируемого объекта для последующей проверки корректности вызова зависимого объекта. Позволяет проверить логику именно нашего тестируемого объекта, без проверок зависимых объектов.
— mock object (мок-объект), очень похож на тестовый шпион, однако не записывает последовательность вызовов с переданными параметрами для последующей проверки, а может сам выкидывать исключения при некорректно переданных данных. Т.е. именно мок-объект проверяет корректность поведения тестируемого объекта.
— fake object (фальшивый объект), используется в основном чтобы запускать (незапускаемые) тесты (быстрее) и ускорения их работы. Эдакая замена тяжеловесного внешнего зависимого объекта его легковесной реализацией. Основные примеры — эмулятор для конкретного приложения БД в памяти (fake database) или фальшивый вебсервис.
Roy Osherove в книге «The Art of Unit Testing» предлагает упростить данную классификацию и оставляет всего три типа тестовых объектов — Fakes, Stubs и Mocks. Причем Fake может быть как stub-ом, так и mock-ом, а тестовый шпион становится mock-ом. Хотя лично мне не очень нравится перемешивание тестового шпиона и мок-объекта.
Запутанно? Возможно. У данной статьи была задача показать, что написание правильных модульных тестов — достаточно сложная задача, и что не всё то mock, что создаётся в посредством mock-frameworks.
Надеюсь, я сумел подвести читателя к основной мысли — написание качественных модульных тестов дело достаточно непростое. Модульные тесты подчиняются тем же правилам, что и основное приложение, которое они тестируют. При написании модульных тестов следует тестировать модули настолько изолированно друг от друга, насколько это возможно, и различные разновидности применяемых тестовых объектов в этом, безусловно, помогают. Стиль написания модульных тестов должен быть таким же, как если бы вы писали само приложения — без копипастов, с продуманными классами, поведением, логикой.
PHPUnit. «Как мне протестировать мой чёртов контроллер», или тестирование для сомневающихся
Да, это очередной пост на тему тестирования. Казалось бы, что тут уже можно обсуждать? Все кому надо — пишут тесты, кому не надо — не пишут, все счастливы! Факт же в том, что большинство постов о юнит-тестировании имеют… как бы так никого не обидеть… идиотские примеры! Нет, ну правда! Сегодня я попытаюсь это исправить. Прошу под кат.
И так, быстрый гуглёж на тему тестов находит просто уйму статей, которые в своей основной массе делятся на две категории:
1) Счастье копирайтера. Сначала мы видим долгое вступление, потом историю юнит-тестирования на Древней Руси, потом десять лайфхаков с тестами, и в конце-концов пример. С тестированием кода вроде этого:
И я сейчас не шучу. Я правда видел статьи с «калькулятором» в роли учебного пособия. Да-да, я понимаю что для начала надо всё упростить, абстракции, туда-сюда… Но ведь на этом всё и заканчивается! А дальше дорисуйте сову, как говорится
2) Чрезмерно переусложнённые примеры. А давайте напишем тест, и запихнём его в Gitlab CI, а потом будем ещё автодеплоить если тест прошёл, а на тесты ещё PHP Infection намажем, да с Hudson всё соеденим. И так далее в таком стиле. Вроде и полезно, а вроде и совсем не то что ты ищешь. А ведь хочется просто чуток увеличить стабильность своего проекта. А все эти непрерывности — ну потом, не всё же сразу.
В итоге люди сомневаются, «а надо ли оно мне». Я же, в свою очередь, хочу попытаться рассказать о тестировании понятнее. И оговорюсь сразу — я разработчик, я не тестировщик. Я уверен, что я сам многого не знаю, а моим первым в жизни словом не было слово «мок». Я даже никогда не работал по TDD! Зато я точно знаю, что даже мой текущий уровень навыков позволил мне покрыть несколько проектов тестами, а эти самые тесты уже отловили свой десяток багов. А если мне это помогло — значит и кому-то ещё может помочь. Некоторые пойманные баги было бы сложно выловить вручную.
Для начала, краткий ликбез в формате вопрос-ответ:
Q: Я обязан использовать какой-то фреймворк? А что если у меня Yii? А если Kohana? А если %one_more_framework_name%?
А: Нет, PHPUnit это самостоятельный фреймворк для тестирования, вы можете его прикрутить хоть к легаси-коду на самопальном фреймворке.
Q: А я сейчас руками сайт по-быстрому прохожу, и нормально. Зачем оно мне?
А: «Прогон» нескольких десятков тестов длится несколько секунд. Автоматическое тестирование всегда быстрее мануального, а при качественных тестах ещё и надёжнее, так как покрывает все сценарии.
Q: У меня легаси-код с функциями по 2000 строк. Я могу это тестировать?
A: И да, и нет. В теории — да, любой код можно покрыть тестом. На практике, код должен писаться с заделом под будущее тестирование. Функция на 2000 строк будет иметь слишком много зависимостей, ветвлений, пограничных случаев. Может и получится её в итоге всю покрыть, но скорее всего это займёт у вас непозволительно много времени. Чем качественнее код — тем легче его тестировать. Чем лучше соблюдается принцип Single Responsibility — тем проще будут тесты. Для тестирования старых проектов чаще всего придётся сначала здорово отрефакторить их.
Q: У меня очень простые методы (функции), что там тестировать? Там всё надёжно, там нет места ошибке!
А: Следует понимать, вы не тестируете правильность реализации функции (если у вас не TDD), вы просто «фиксируете» её текущее состояние работы. В будущем, когда вам понадобится её изменять, вы сможете с помощью теста быстро определять не сломали ли вы её поведение. Пример: есть функция, которая валидирует email. Делает она это регуляркой.
Весь ваш код расчитывает на то, что если передать в эту функцию валидный имейл — она вернёт true. Массив валидных имейлов — тоже true. Массив хотя бы с одним невалидным имейлом — false. Ну и так далее, по коду суть понятна. Но настал день, и вы решили заменить монструозную регулярку внешним API. Но как гарантировать, что переписанная функция не поменяла принцип работы? Вдруг она плохо обработает массив? Или вернёт не boolean? А тесты смогут это всё держать под контролем. Хорошо написанный тест сразу укажет на поведение функции отличное от ожидаемого.
Q: Когда я начну видеть толк от тестов?
А: Во-первых, как только покроете значительную часть кода. Чем ближе покрытие к 100% — тем надёжнее тестирование. Во-вторых, как только придётся делать глобальные изменения, либо же изменения в сложной части кода. Тесты могут отловить такие проблемы, которые вручную могут быть легко упущены (пограничные случаи). Во-третьих, при написании самих тестов! Часто возникает ситуация, когда при написании теста выявляются недостатки кода, которые на первый взгляд незаметны.
Q: Ну вот, у меня сайт на laravel. Сайт это не функция, сайт это хренова гора кода. Как тут тестировать?
А: Именно об этом пойдёт речь дальше. Вкратце: отдельно тестируем методы контроллеров, отдельно middleware, отдельно сервисы, и т. д.
Одна из идей Unit-тестирования заключается в изоляции тестируемого участка кода. Чем меньше кода проверяется одним тестом — тем лучше. Посмотрим пример максимально приближённый к реальной жизни:
Эмуляция, подмена объектов называется моканьем (от англ. mock object, буквально: «объект-пародия»). Никто не мешает писать такие объекты вручную, но всё уже придумано до нас, поэтому на помощь приходит такая чудесная библиотека как Mockery. Давайте создадим моки для сервисов.
Ничего сложного, не так ли? Мы создали заглушки для необходимых параметров класса, создали экземпляр нужного класса, и «дёрнули» нужный метод, передавая заведомо неправильный запрос. Получили ответ. Но как теперь его проверить? Это и есть самая важная часть теста — так называемое утверждение, assertion. PHPUnit имеет десятки готовых assertions. Просто используем одну из них
Данный тест гарантирует следующее — если в метод логин прилетит аргумент-объект у которого не найдётся поля login или password — то метод вернёт строку «Auth error». Вот, в общем-то, и всё. Так просто — но так полезно, ведь теперь мы можем редактировать метод login без страха сломать что-то. Наш frontend может быть уверенным, что в случае чего — он получит именно такую ошибку. И если кто-то сломает это поведение (например, решит изменить текст ошибки) — то тест сразу же об этом просигнализирует! Допишем остальные проверки, чтобы покрыть как можно больше возможных сценариев.
А что же по-поводу зависимостей, спросите вы. Их то мы «заглушили», а вдруг они сломаются? А вот именно для этого и надо максимальное покрытие кода тестами. Мы не будем проверять работу этих сервисов в контексте логина — мы будем тестировать логин рассчитывая на правильную работу сервисов. А потом напишем такие же, изолированные тесты для этих сервисов. А потом тесты для их зависимостей. И так далее. В итоге каждый отдельный тест гарантирует только правильную работу маленького куска кода, при условии что все его зависимости работают правильно. А так как все зависимости тоже покрыты тестами — то их правильная работа тоже гарантируется. В итоге, любое изменение в систему ломающее логику работы даже малейшего участка кода — сразу же отобразится в том или ином тесте. Как конкретно запустить прогон тестов — рассказывать не буду, документация у PHPUnit вполне хорошая. А в Laravel, например, достаточно выполнить vendor/bin/phpunit с корня проекта, чтобы увидеть сообщение вроде этого
— все тесты прошли успешно. Или вроде этого
Один из семи assert-ов провалился.
«Это, конечно, классно, но что из этого я не поймаю руками?» — спросите вы. А давайте для этого представим следующий код
Мы видим упрощённую модель работы с внешним API. Функция использует какой-то класс для работы c API, и в случае ошибки — возвращает null. Если же при использовании этой функции мы получаем null — следует «поднять панику» (отправить сообщение в слак, или имейл разработчику, или кинуть ошибку в кибану. Да куча вариантов). Вроде всё просто, не так ли? Но представим что через некоторое время другой разработчик решил «поправить» эту функцию. Он решил что возвращать null — это прошлый век, и следует кидать исключение.
И он даже переписал все участки кода, где вызывалась эта функция! Все, кроме одного. Его он упустил. Отвлёкся, устал, просто ошибся — да мало ли. Факт лишь в том, что один участок кода всё ещё ожидает старого поведения функции. А PHP это у нас не Java — мы не получим ошибку компиляции на основании того, что throwable функция не завёрнута в try-catch. В итоге в одном из 100 сценариев использования сайта, в случае падения API — мы не получим сообщение от системы. Более того, при ручном тестировании мы скорее всего не отловим этот вариант события. API у нас внешнее, от нас не зависит, работает хорошо — и скорее всего мы не попадём «руками» на случай отказа API, и неверной обработки исключения. Зато будь у нас тесты — они отлично отловят данный кейс, потому что класс ExternalApi в ряде тестов у нас «заглушен», и эмулирует как нормальное поведение, так и падение. И следующий тест у нас упадёт
Этой информации, на самом деле, достаточно. Если у вас не легаси лапша, уже спустя минут 20-30 вы сможете написать свой первый тест. А спустя несколько недель — узнать что-то новое, крутое, вернуться в комментарии под этот пост, и написать какой автор говнокодер, и не знает о %framework_name%, и тесты хреновые пишет, а надо делать %this_way%. И я буду очень рад в таком случае. Это будет значит что моя цель достигнута: кто-то ещё открыл для себя тестирование, и немножечко повысил общий уровень профессионализма в нашей сфере!
Unit-тестирование от начинающего начинающим
На написание статьи меня сподвигнул этот пост. В нём приведено описание инструментов и некоторая теоретическая информация.
Сам я только начинаю разбираться в unit-тестировании и тестировании вообще, поэтому решил поделиться некоторой информацией касательно этого дела. А также систематизировать свои знания и навыки. Далее постараюсь объяснить процесс тестирования по шагам простым обывательским языком, так как нигде в интернете не нашёл разжёванного описания, по шагам так сказать. Кому интересно и кто хочет попробовать всё-таки разобраться, добро пожаловать.
Что такое автоматизированное тестирование и unit-тестирование я писать не буду, для этого есть википедия.
Для наших тестов будем использовать, наверное самый популярный фрэймворк – PHPUnit. Для начала нам необходимо его утановить. Делать это проще всего через PEAR. Как это сделать, написано в документации. Используется две команды(из документации):
Естественно, путь к PEAR должен быть прописан в PATH. Когда загрузятся необходимые файлы, наш PHPUnit будет полностью готов к тестированию нашего кода.
Let’s Rock
Итак, начнём. Пусть у нас будет какая-то модель данных. В ней два атрибута: строка и число. Есть метод-сеттер и методы для сохранения и загрузки значений (в файл).
Мы определили базовые методы и атрибуты классов. Так как у нас пока ничего не читается и не пишется, по условию возвращаем false.
Теперь отложим на время нашу модель и займёмся тестом. Тест представляет собой обычный класс, унаследованный от базового класса (в нашем случае PHPUnit_Framework_TestCase). Методы этого класса, и есть тесты. Создадим папку unit для нашего теста.
TestModelTest — наш тест-класс для класса TestModel.
testTrue() — непосредственно тест. В нём мы определяем сценарии для конкретных случаев. В данном тесте мы просто проверим, что true является true 🙂 Это делается при помощи метода assertTrue (assert-англ-утверждать). Т.е. мы утверждаем, что true является истинной.
Запустим наш тест. PHPUnit достаточно указать папку, в которой лежат все наши тесты.
Ура, наш тест работает! Идём далее.
TDD – Test Driven Development – подход, при котором, грубо говоря, сначала пишутся тесты, а потом постепенно, исходя из них, пишется основной класс. Подробнее в википедии. Пойдём этим путём. Каркас модуля у нас уже есть. Требования тоже. Теперь напишем тестовые случаи, исходя из наших требований.
Мы описали все три случая в трёх методах. Для каждого свой. Теперь запустим тесты:
Damn! Ну ничего, так и должно быть 🙂 Теперь добавим немного кода в нашу модель.
Думаю, в коде ничего не должно вызывать затруднений.
Уже лучше. Уже проходит в два раза больше проверок. Идём по порядку:
1. testStringCannotBeEmpty. Строка не может быть пустой. Добавляем проверку:
2. testIntMustBeGreaterThanTenAdnSmallerThanTwenty. Условие 10 setAttributes(20,’test3′); Мы не рассмотрели крайний случай! Исправляем:
Запускаем наши тесты:
Ура, все три теста прошли. Наша модель удовлетворяет поставленным требованиям. Что и требовалось 🙂
Чистые тесты на PHP и PHPUnit
В экосистеме PHP существует много инструментов, обеспечивающих удобное тестирование на PHP. Одним из самых известных является PHPUnit, это почти синоним тестирования на этом языке. Однако о хороших методиках тестирования пишут не так много. Есть много вариантов, для чего и когда писать тесты, какого рода тесты, и так далее. Но, честно говоря, не имеет смысла писать тест, если позднее вы не сможете его прочесть.
Тесты — это особая разновидность документации. Как я ранее писал о TDD на PHP, тест всегда будет (или хотя бы должен) ясно говорить о том, в чём заключается задача конкретной части кода.
Если один тест не может выразить эту идею, то тест плохой.
Я подготовил набор методик, которые станут подспорьем для PHP-разработчиков в написании хороших, удобочитаемых и полезных тестов.
Начнём с основ
Есть набор стандартных методик, которым многие следуют без каких-либо вопросов. Многие из них я упомяну и попытаюсь объяснить, для чего они нужны.
1. Тесты не должны содержать операций ввода-вывода
Основная причина: операции ввода-вывода медленные и ненадёжные.
Медленные: даже если у вас самое лучшее в мире железо, операции ввода-вывода всё равно будут медленнее обращений к памяти. Тесты всегда должны работать быстро, иначе люди будут слишком редко их запускать.
Ненадёжные: некоторые файлы, бинарники, сокеты, папки и DNS-записи могут быть недоступны на некоторых машинах, на которых вы проводите тестирование. Чем больше вы полагаетесь при тестировании на операции ввода-вывода, тем больше ваши тесты привязаны к инфраструктуре.
Какие операции относятся к вводу-выводу:
Изолируйте тесты так, чтобы им не нужны были операции ввода-вывода: ниже я привёл архитектурное решение, которое предотвращает выполнение тестами операций ввода-вывода за счёт разделения ответственности между интерфейсами.
При начале тестирования с помощью этого метода будет создан локальный файл, и время от времени будут создаваться его снимки:
Для этого нам нужно настроить предварительные условия запуска тестов. На первый взгляд всё выглядит разумно, но на самом деле это ужасно.
Пропуск теста из-за того, что не выполнены предварительные условия, не обеспечивает качество нашего ПО. Это лишь скроет баги!
Исправляем ситуацию: изолируем операции ввода-вывода, переложив ответственность на интерфейс.
Теперь я знаю, что JsonFilePeopleProvider в любом случае будет использовать ввод-вывод.
Вместо file_get_contents() можно использовать слой абстракции вроде файловой системы Flysystem, для которой легко сделать заглушки.
2. Тесты должны быть осознанными и осмысленными
Основная причина: тесты — это разновидность документации. Поддерживайте их понятность, краткость и удобочитаемость.
Понятность и краткость: ни беспорядка, ни тысяч строк заглушек, ни последовательностей утверждений.
Удобочитаемость: тесты должны рассказывать историю. Для этого отлично подходит структура «дано, когда, тогда».
Характеристики хорошего и удобочитаемого теста:
Повторюсь: дело не в покрытии, а в документировании.
Вот пример сбивающего с толку теста:
Давайте адаптируем формат «дано, когда, тогда» и посмотрим, что получится:
Как и раздел «дано» (Given), «когда» и «тогда» можно перенести в приватные методы. Это сделает ваш тест более удобочитаемым.
Теперь в assertEquals бессмысленный беспорядок. Читающий это человек должен проследить утверждение, чтобы понять, что оно означает.
Теперь всё предельно понятно! Если человек не имеет крыльев, он не должен уметь летать! Читается как стихотворение
Теперь раздел «Further cases», который дважды появляется в нашем тексте, является ярким свидетельством того, что тест делает слишком много утверждений. При этом метод testCanFly() совершенно бесполезен.
Давайте снова улучшим тест:
3. Тест не должен зависеть от других тестов
Основная причина: тесты должны запускаться и успешно выполняться в любом порядке.
Я не вижу достаточных причин для создания взаимосвязей между тестами. Недавно меня попросили сделать тест функции входа в систему, приведу его здесь в качестве хорошего примера.
Это плохо по нескольким причинам:
Также нам понадобится добавить тесты для аутентификации и т. д. Эта структура так хороша, что в Behat используется по умолчанию.
4. Всегда внедряйте зависимости
Основная причина: очень дурной тон — создавать заглушку для глобального состояния. Отсутствие возможности создавать заглушки для зависимостей не позволяет тестировать функцию.
Полезный совет: забудьте o статичных stateful-классах и экземплярах синглтонов. Если ваш класс от чего-то зависит, то сделайте так, чтобы его можно было внедрять.
Вот грустный пример:
Как можно протестировать этот ранний ответ?
Для его тестирования нам нужно понимать поведение класса Cookies и быть уверенными в том, что можем воспроизвести всё связанное с этим окружение, в результате получив определённые ответы.
Ситуацию можно исправить, если внедрить экземпляр Cookies в качестве зависимости. Тест будет выглядеть так:
Совершенно нормально менять архитектуру, чтобы облегчить тестирование! А создавать методы для облегчения тестирования — не нормально.
5. Никогда не тестируйте защищённые/приватные методы
Основная причина: они влияют на то, как мы тестируем функции, определяя сигнатуру поведения: при таком-то условии, когда я ввожу А, то ожидаю получить Б. Приватные/защищённые методы не являются частью сигнатур функций.
Я даже не хочу показывать способ «тестирования» приватных методов, но дам подсказку: вы можете это сделать только с помощью API reflection.
Всегда как-нибудь наказывайте себя, когда задумываетесь об использовании reflection для тестирования приватных методов! Плохой, плохоооой разработчик!
По своему определению, приватные методы вызываются только изнутри. То есть они не доступны публично. Это означает, что вызывать подобные методы могут только публичные методы из того же класса.
Если вы протестировали все свои публичные методы, то вы также протестировали все приватные/защищённые методы. Если это не так, то свободно удаляйте приватные/защищённые методы, их всё-равно никто не использует.
Продвинутые советы
Надеюсь, вы ещё не заскучали. Всё-таки про основы нужно было рассказать. Теперь поделюсь своим мнением о написании чистых тестов и решениях, влияющих на мой процесс разработки.
Самое важное, о чём я не забываю при написании тестов:
1. Тесты в начале, а не в конце
Ценности: учёба, быстрое получение обратной связи, документирование, рефакторинг, проектирование в ходе тестирования.
Это основа всего. Важнейший аспект, включающий в себя все перечисленные ценности. Когда вы заранее пишете тесты, это помогает вам сначала понять, как должна быть структурирована схема «дано, когда, тогда». При этом вы сначала документируете, и, что ещё важнее, запоминаете и задаёте свои требования как самые важные аспекты.
Странно слышать о том, чтобы писать тесты до реализации? А представьте, насколько странно реализовать что-то, а при тестировании выяснить, все ваши выражения «дано, когда, тогда» не имеют смысла.
Также этот подход позволит проверять ваши ожидания каждые две секунды. Вы получаете обратную связь максимально быстро. Вне зависимости от того, насколько большой или маленькой выглядит фича.
Зелёные тесты — идеальная область для рефакторинга. Главная мысль: нет тестов — нет рефакторинга. Рефакторинг без тестов просто опасен.
Наконец, задав структуру «дано, когда, тогда», вам станет очевидно, какие интерфейсы должны быть у ваших методов и как они должны себя вести. Соблюдение чистоты теста также заставит вас постоянно принимать разные архитектурные решения. Это заставит вас создавать фабрики, интерфейсы, нарушать наследования и т. д. И да, тестировать станет легче!
Если ваши тесты — это живые документы, объясняющие работу приложения, то крайне важно, чтобы они делали это понятно.
2. Лучше без тестов, чем с плохими тестами
Ценности: учёба, документирование, рефакторинг.
Многие разработчики думают о тестах так: напишу фичу, буду гонять фреймворк для тестирования до тех пор, пока тесты не покроют определённое количество новых строк, и отправлю в эксплуатацию.
Мне кажется, нужно уделять больше внимания ситуации, когда новый разработчик начинает работать с этой фичей. О чём расскажут тесты этому человеку?
Если ваши тесты представляют собой всего лишь беспорядочный код, который заставляет фреймворк покрывать больше строк, с примерами, которые не имеют смысла, то пришло время остановиться и подумать, стоит ли вообще писать эти тесты.
Помните: ваши тесты — живые документы, пытающиеся объяснить, как должно вести себя ваше приложение. assertFalse($a->canFly()) мало что документирует. А assertFalse($personWithNoWings->canFly()) — уже достаточно много.
3. Навязчиво прогоняйте тесты
Ценности: учёба, быстрое получение обратной связи, рефакторинг.
Прежде чем начать работу над фичей, запустите тесты. Если они сбоят до того, как вы принялись за дело, то вы узнаете об этом до того, как напишете код, и вам не придётся тратить драгоценные минуты на отладку сломанных тестов, о которых вы даже не беспокоились.
После сохранения файла запустите тесты. Чем раньше вы узнаете о том, что что-то поломалось, тем быстрее исправите и двинетесь дальше. Если прерывание рабочего процесса для решения проблемы вам кажется непродуктивным, то представьте, что позднее вам придётся вернуться на много шагов назад, если вы не будете знать о возникшей проблеме.
Поболтав пять минут с коллегами или проверив уведомления с Github, запустите тесты. Если они покраснели, то вы знаете, на чём остановились. Если тесты зелёные, можно работать дальше.
После любого рефакторинга, даже имён переменных, запустите тесты.
Серьёзно, запускайте чёртовы тесты. Так же часто, как вы нажимаете кнопку «сохранить».
PHPUnit Watcher может делать это за вас, и даже отправлять уведомления!
4. Большие тесты — большая ответственность
Ценности: учёба, рефакторинг, проектирование в ходе тестирования.
В идеале, каждый класс должен иметь один тест. Этот тест должен покрывать все публичные методы в этом классе, а также каждое условное выражение или оператор перехода…
Можно считать примерно так:
Чем больше у вас будет публичных методов, тем больше понадобится тестов.
Никто не любит читать длинную документацию. Поскольку ваши тесты тоже документы, то маленький размер и осмысленность будут только увеличивать их качество и полезность.
Также это важный сигнал о том, что ваш класс накапливает ответственности и пришло время отрефакторить его, перенеся ряд функций в другие классы, или перепроектировав систему.
5. Поддерживайте набор тестов для решения проблем с регрессией
Ценности: учёба, документирование, быстрое получение обратной связи.
Вы думаете, что кто-то передаёт «10», но на самом деле передаётся «10 bananas». То есть приходят два значения, но одно лишнее. У вас баг.
Что вы сделаете в первую очередь? Напишете тест, который обозначит такое поведение ошибочным.
Конечно, тесты ничего не передают. Но теперь вы знаете, что нужно сделать, чтобы они передавали. Исправьте ошибку, сделайте тесты зелёными, разверните приложение и будьте счастливы.
Сохраните у себя этот тест. По возможности, в наборе тестов, предназначенных для решения проблем с регрессией.
Вот и всё! Быстрое получение обратной связи, исправление багов, документирование, устойчивый к регрессии код и счастье.