Как тестировать код php
AspectMock — тестируем любой PHP код
Как часто вы пишете велосипеды? Можно я тоже тут рядом пристроюсь? Но дело в том, что мой велосипед, он особенный. Казалось бы, простенький, трехколесненький, склепанный всего за один день. Но есть одна хитрость — он работает на движке от болида. Что он умеет?
О нет, мы изменили синглтон! Мы переопределили статический метод. Как же теперь жить?
Но вопрос теперь в другом: как мы жили до этого?
Встречайте AspectMock. Самый простой, но самый мощный фреймворк для моков и стабов на PHP.
Ваш новый суперпростой помощник в тестировании. Основан на Go AOP от NightTiger.
Итак, идея AspectMock достаточно простая: позволить тестировать всё то что в PHP ранее считалось «плохими практиками» по причине невозможности тестирования. Статические методы, синглтоны, методы класса — всё это теперь поддается изменениям в реальном времени. Можно проверить вызов любого метода. Вот пример:
Увы, но мы не можем сделать юнит тест для этого метода классическими средствами. Чтобы мы не делали, а будет вызван метод save(), который обратится в базу данных. Впрочем, в AspectMock это вообще не проблема.
Метод save был вызван, но он был заменен пустышкой. Мы довольны, база цела, имя пользователя присвоено. Покрытие 100%, изоляция присутствует.
Зачем это всё?
В PHP сложилась странная практика считать любой нетестируемый код плохим. Пример выше показывает, что использование паттерна ActiveRecord продуцирует такой нетестируемый код. Плохой ли он? Ну неправда ведь. Хороший паттерн, реализован во многих ORM на разных яхыках.
Практика странная тем, что «тестируемость» кода определяется только техническими ограничениями самого языка PHP.
И потому, прежде чем писать какой либо код, нужно сразу держать в голове: как мы будем это тестировать. И обязательно использовать Dependency Injection. Обязательно.
А теперь на секундочку давайте представим, что почти любой ООП код на PHP поддается юнит-тестировнию. Может лучше сосредоточимся на читабельности кода и его эффективности, вместо того, чтобы плодить лишние сервисы, инжектить их, а потом создавать на них моки многострочными конструкциями в PHPUnit?
Впрочем, реальность такова, что большинство разработчиков относятся к этому вопросу проще. Они вообще не пишут юнит тесты.
AspectMock позволит вам сосредоточиться на написании эффективного кода. Конечно, вы должны соблюдать правильную архитектуру, правильные стандарты, следить за использованием зависимостей, но вам больше не стоит себя искусственно ограничивать техническими возможностями PHP. Как говорил один замечательный человек: «Ничто не истина. Всё дозволено.»
Как это работает?
Сам AspectMock очень простой: всего 8 файлов. Там практически нечего изучать. Зато Go AOP, о котором вы уже могли читать на Хабре, предоставляет отличную платформу для того, чтобы встраиваться в любые методы приложения посредством pointcut’ов. На основе наших предпочтений мы можем подменять их своими пустышками, а также регистрировать их выполнение. Go AOP в реальном времени создает прокси классы и встраивает их в иерархию. Работает посредством изменения автолоадинга. Совместим со всеми популярными фреймворками: Symfony2, Zend Framework 2, Yii, Laravel. Если вы ещё не знакомы с Go Aop, очень рекомендую с ним поиграться.
Где применять AspectMock?
Ну точно что не в продакшне. AspectMock создан исключительно для тестирования, он устанавливается через композер и работает в PHPUnit и Codeception. Насколько AspectMock стабилен? Ровно настолько же, насколько и сам Go AOP. Как понимаете, проект пока очень даже экспериментальный. Текущая версия 0.1.0. Самое сложное — первичная установка. Попробуйте, если же у вас всё завелось, дальше должен работать с пол пинка.
Будут очень интересны ваши отзывы. Спасибо за внимание.
Upd: Появилось видео, где демонстрируется AspectMock в действии
Обязательно взгляните, лучше раз посмотреть, чем много раз читать.
Легкий способ начать тестировать
Если вы PHP-разработчик, и по разным обстоятельствам тесты для своих приложений не пишете, то эта статья для вас. В ней я постараюсь вкратце показать с чего начать и что делать, чтобы написание тестов приносило вам радость, а вашему приложению стабильность.
Итак, первый совет. Забудьте всё что вы знаете о юнит-тестах. Швырните табуреткой в человека, который сказал вам, что без них не обойтись. Попробуем разобраться, в каких случаях нужно их использовать, а в каких — нецелесообразно.
Я абсолютно уверен, что PHP-программисты редко пишут тесты, потому что начинают не с того конца. Все знают, что тесты это хорошо и клево. Но открыв сайт того же PHPUnit, и прочитав красочный пример о тестировании калькулятора умеющего выполнять операцию a + b, они спрашивают себя: какое отношение этот калькулятор имеет к моему приложению? Ответ: никакого. Ровно как все похожие примеры, на сайтах других фреймворков тестирования. Будто бы все забыли, что PHP прежде всего для веба. Будто бы все пишут калькуляторы, а не сайты на основе MVC-парадигмы.
Положим, вы создаете сайт или разрабатываете веб-приложение. На нем уже есть некоторые страницы, формы, возможно даже интерактивные элементы. Как вы (или, допустим, ваш заказчик) проверяете что сайт работает? Наверняка вы заходите на сайт, кликаете по ссылкам, заполняете формы, смотрите на результат. И по-хорошему, все эти рутинные процессы кликанья и заполнения форм, стоит автоматизировать в первую очередь.
И потому мы поговорим о функциональных тестах (или приемочных).
Функциональное тестирование — это тестирование ПО в целях проверки реализуемости функциональных требований, то есть способности ПО в определённых условиях решать задачи, нужные пользователям. Функциональные требования определяют, что именно делает ПО, какие задачи оно решает.
Почему написание тестов стоит начать именно с них? Да просто, чтобы быть уверенным, что ваш сайт функционирует, и всё там работает. Приемочные тесты одинаково хорошо подойдут как для солидного веб-приложения, так и простенького сайта, склепанного за ночь на коленке. А потому начать стоит с них.
Что у нас есть для функционального тестирования? Из всего известного мне, это Codeception и, например, PHPUnit + Mink или прямое использование Selenium. Я хотел включить в этот печерень и Behat, но его автор попросил не использовать Behat для функционального тестирования.
Если бы тестировать с Selenium или PHPUnit + Mink было просто, вы бы уже наверняка их использовали. Потому остановимся на Codeception.
Тест в нем описывается так:
Всего в несколько строчек вы проверяете, что как минимум, главная страница вашего сайта открывается. За длительной работой, сломать её, не так и сложно.
Точно так же, вы можете описывать дальнейшие действия:
Вот тут мы уже проверяем, что форма отзывов на вашем сайте работает. По крайней мере, пользователю, показывается, что его мнение было учтено.
А теперь попробуем определиться, с unit-тестами.
Модульное тестирование, или юнит-тестирование (англ. unit testing) — процесс в программировании, позволяющий проверить на корректность отдельные модули исходного кода программы.
Идея состоит в том, чтобы писать тесты для каждой нетривиальной функции или метода. Это позволяет достаточно быстро проверить, не привело ли очередное изменение кода к регрессии, то есть к появлению ошибок в уже оттестированных местах программы, а также облегчает обнаружение и устранение таких ошибок.
Беда их использования в том, что обычно веб-приложения основываются на CMS или фреймворках. И порой не всегда получается выделить конкретные модули для тестирования. Особенно неудобно получается, когда логика приложения раскидана по различным классам, а каждый конкретный юнит по сути не делает ничего. Процесс тестирования ещё будет усложнен тем, что все модули как-то взаимосвязаны, имеют много унаследованых методов, конфигураций, и выделить где код ваш, а где код библиотеки порой очень трудно.
Но если в рамках функциональных тестов вы проверили всё, что пользователь может сделать, то в юнит-тестах постарайтесь учесть, что он не должен сделать.
Например, проверьте, не позволяет ли ваш код создавать двух пользователей с одинаковыми именами. Проверьте, что при попытке создать дубликат приложение не упадет, и не покажет белый экран смерти, а перехватит ошибку и покажет её текст пользователю. Согласитесь, пользователь, может натворить, гораздо больше, чем вы того хотите. А потому предусмотреть все возможные сценарии и покрыти их функциональными тестами порой невозможно.
Или ещё один пример: у вас на сайте акция — каждый 100ый зарегистрированый пользователь получает подарок. Как бы так проверить, что подарки выдаются, и не создавать при этом 100 лишних пользователей? Вот тут уже пишите юнит-тесты. Без них, скорее всего, никак.
Если в вашем приложении четко выражена бизнес-логика, например: «этот метод создает пользователя, а этот метод позволяет менять статус группы», то вполне резонно покрыть и такие методы юнит-тестами.
И ещё: если вы создаете сайт или приложение на основе своего фреймворка, CMS, или каких-то своих модулей к этим фреймворкам и CMS — обязательно пишите к ним модульные тесты. Тут без вариантов. Если же вы используете сторонние популярные решения, а не изобретаете велосипеды, то скорее всего их код уже протестирован автором и зацикливаться на их тестировании не стоит.
Резюмируя, я бы сказал так: тесты действительно нужны и они не так страшны, как вам могут представляться. Начните написание тестов с приемочных, продолжте модульными. Тестируйте то, что важно и тестируйте то, в чем не уверены.
Disclaimer: прошу прощения за ряд намеренных упрощений и неточностей в статье. С моей точки зрения, они необходимы, чтобы объяснить базовые принципы автоматического тестирования PHP-приложений. Чем больше людей будут тестировать свои приложения, тем лучше станет мир. Ведь главное — начать.
Как писать легко тестируемый и поддерживаемый код на PHP
Разнообразные фреймворки предоставляют инструменты для быстрой разработки приложений, но зачастую они способствуют накоплению технического долга также быстро, как позволяют создавать функциональность.
Технический долг появляется, когда сопровождение не является главной целью разработчика. Будущие изменения и отладка кода становятся затруднительными в связи с недостаточным модульным тестированием и непроработанной структурой.
В этой статье вы узнаете, как структурировать ваш код так, чтобы достичь простоты тестируемости и сопровождения – и сэкономить ваше время.
Давайте начнем с несколько выдуманного, но типичного кода. Это может быть класс в любом заданном фреймворке:
Этот код будет работать, но требует некоторых улучшений:
1. Этот код не тестируемый.
2. Данный код не такой поддерживаемый, каким бы мог быть.
Например, если мы изменим источник данных, нам нужно будет изменять код базы данных при каждом использовании App::db в нашем приложении. Кроме того непонятно, как быть в случае, когда мы хотим использовать не просто информацию о текущем пользователе?
Предпринятый модульный тест
Здесь приведена попытка создания модульного теста для функционала, описанного выше:
Для того чтобы этот модульный тест работал, нам нужно сделать следующее:
Что ж, давайте перейдем к тому, как это можно улучшить.
Придерживаемся принципа «Не повторяйся»
Этим методом мы можем пользоваться во всем нашем приложении. Мы можем передать текущего пользователя в вызове функции, вместо того, чтобы внедрять данный функционал в нашу модель. Код становится более модульным и поддерживаемым, когда перестает зависеть от других функциональных единиц (например, глобальной переменной сессии).
Однако он все еще не такой легко тестируемый и поддерживаемый, каким бы мог быть. Мы до сих пор полагаемся на соединение с базой данных.
Внедрение зависимости
Давайте поправим ситуацию путем добавления некоторых зависимостей. Здесь показано, как может выглядеть наша модель, если мы поместим соединение с базой данных в класс:
Теперь зависимости для нашей модели User обеспечены. Наш класс больше не предполагает наличие определенного подключения к базе данных, и не полагается на какие-либо глобальные объекты.
На данный момент наш класс в целом тестируемый. Мы можем передать источник данных согласно нашему выбору (по большей части) и идентификатор пользователя и протестировать результаты вызова. Также мы можем переключать соединение к нашим отдельным базам данных (предполагая, что обе базы реализуют одинаковые методы извлечения данных). Круто!
Давайте посмотрим, как выглядит модульный тест сейчас:
Я добавил кое-что новенькое в этот модульный тест: фиктивную реализацию. Фиктивная реализация позволяет нам имитировать (фальсифицировать) PHP объекты. В данном случае мы осуществляем фиктивную реализацию подключения к базе данных. С нашей « заглушкой » мы можем пропустить тестирование подключения к базе данных и просто проверить нашу модель.
Хотите узнать больше о фиктивной реализации?
Таким образом, решаются несколько проблем:
Но мы все еще можем сделать наш код гораздо лучше. С этого места начинается самое интересное.
Интерфейсы
Для дальнейшего улучшения мы можем определить и реализовать интерфейсы. Рассмотрим следующий код:
Здесь происходит несколько вещей.
Что мы имеем в результате?
А еще мы упростили наш модульный тест!
Но мы все еще можем сделать код лучше!
Контейнеры
Рассмотрим использование нашего текущего кода:
Я переместил создание модели User в одно место в конфигурации приложения. В результате:
Заключительное слово
В нашем уроке мы выполнили следующее:
Я уверен, вы заметили, что мы добавили намного больше кода для достижения простоты обслуживания и тестируемости. Сильный аргумент против такой реализации – увеличение сложности. В самом деле, для этого требуется более глубокое знание кода, как для основного разработчика, так и для остальных участников проекта.
Однако, затраты на объяснение и понимание кода с лихвой окупаются снижением технического долга.
Код стал значительно более легким в обслуживании, представилась возможность производить изменения всего в одном месте, а не в нескольких.
Возможность быстро проводить модульное тестирование снизит количество ошибок в коде со значительным отрывом – особенно в долгосрочных или разрабатываемых в сообществе (с открытым исходным кодом) проектах.
Проделывая дополнительную работу сейчас, мы сэкономим время и освободимся от головной боли в будущем.
Источники
Затем вы можете установить ваши зависимости, основанные на Composer, со следующими требованиями:
При использовании PHP фреймворка Laravel 4 применение контейнеров и других идей, описанных здесь, носит исключительно важный характер.
Автоматическая проверка кода для PHP
Разрешите представить Вам перевод статьи Johannes Schmitt Automated Code Reviews for PHP. Лично мне она помогла несколько иначе взглянуть на процесс разработки и тестирования своих приложений. А оригинальный подход автора к тестированию, как минимум, заслуживает внимания.
Если вам тоже интересно, добро пожаловать под кат.
С тех пор как появился Trevis, вы можете в мгновение ока внедрить непрерывную интеграцию во все свои PHP-проекты. Это помогает не только улучшить качество кода, но и существенно упрощает поддержку библиотек, предоставляя информацию о сборке прямо в запрос на обновление(pull request) и, тем самым, уменьшает время получения обратной связи. Travis очень хорош, но, как и другие инструменты тестирования, страдает от наследственной болезни — что бы что-то сделать ему нужны тесты. Готовь биться об заклад, что у вас нет ни одного проекта честно покрытого тестами на 100% или, даже, близко к этому. Это я еще надеюсь, что тесты вы пишите.
Как вам возможно известно, я поддерживаю значительное число плагинов(bundles) для Symfony2 и самостоятельных PHP-библиотек. И благодаря сообществу(спасибо ребята, так держать) я постоянно получаю запросы на обновление в свои репозитории. Некоторые из запросов совершенно бесполезные, некоторые заслуживают внимания, некоторые можно добавлять в основную ветку. Но как бы тщательно не проверялся запрос, время от времени случается так, что добавляется то что не работает или работает, но не всегда.
Пару месяцев назад я попытался изменить эту ситуацию, идея была довольно простой: создать систему которая проверяет код запроса на обновление и дает обратную связь. Я довольно быстро сделал прототип и добавил в него пару простых проверок. Затем, захотел добавить более сложны, например, проверку может ли метод быть вызван. Что бы понять пользу такой проверки, посмотрите на следующий пример:
Type Inference of PHP Code
Я потратил довольно много времени вникая в концепции потока данных, потока управления и абстрактной интерпретации. Что само по себе выглядит довольно сложно и выходит за рамки этой статьи. Но позвольте мне привести всего несколько примеров и дать вам общее представление об этих концепциях.
Анализ потока управления(Control Flow Analysis)
Анализ этого потока позволяет определить в каком порядке будут выполняться различные блоки вашего кода.
Для этого кода поток управления будет выглядеть так:
Мы начинаем в if, затем двигаемся к «foor» или «bar» и, наконец, выходим. Само по себе нам это вряд ли чем-то поможет, но это послужит основой для следующего шага.
Анализ потока данных(Data Flow Analysis)
Анализ потока данных позволяет определить как изменяется контекст выполнения пока мы движемся по схеме которую определили в анализе потока управления.
Абстрактная интерпретация(Abstract Interpretation)
Для нашего случая эта концепция сводится к вопросу «Какие предположения мы можем сделать, если знаем результат условного выражения?». Давайте взглянем на другой пример:
Автоматическая система проверки
Какой от всего этого толк, спросите вы. В начале статьи я сказал, что моей целью было создание автоматической системы проверки кода. И я думаю что сейчас она готова для широкого использования и обсуждения. Я протестировал своей системой ведущие PHP библиотеки, такие как, Zend Framework 2, Symfony2, Doctrine, Propel и многие другие. Она содержит более 100 правил проверки, которые вы можете использовать и конфигурировать. Если у вас есть PHP-проект на Github вы можете легко попробовать. Просто залогинтесь http://jmsyst.com/automated-code-reviews и выберете нужный репозиторий. А если не понравиться, можете выключить в любое время.
Если теперь кто-то скажет, что PHP-программисты не слишком серьезно относятся к качеству кода, отправляйте их их ко мне.
Чистые тесты на 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». То есть приходят два значения, но одно лишнее. У вас баг.
Что вы сделаете в первую очередь? Напишете тест, который обозначит такое поведение ошибочным.
Конечно, тесты ничего не передают. Но теперь вы знаете, что нужно сделать, чтобы они передавали. Исправьте ошибку, сделайте тесты зелёными, разверните приложение и будьте счастливы.
Сохраните у себя этот тест. По возможности, в наборе тестов, предназначенных для решения проблем с регрессией.
Вот и всё! Быстрое получение обратной связи, исправление багов, документирование, устойчивый к регрессии код и счастье.