разработка через тестирование php
Разработка через тестирование (TDD) — PHP: Автоматическое тестирование
В какой момент лучше писать тесты? Вообще, существует три подхода:
В некоторых ситуациях, особого выбора нет. Например, при системном тестировании, когда тест имитирует поведение пользователей и выполняет действия в браузере. Такие тесты пишутся после кода.
В тестах более низкого уровня, интеграционных и модульных тестах, обычно можно выбирать из вариантов описанных выше. И подход «писать тесты после кода» относится к наименее полезным. Почему?
Сам процесс написания кода связан с постоянным запуском кода и проверкой того, что он работает. В самых простых, например, учебных задачах, этот запуск происходит довольно быстро.
В реальном коде подготовка данных для проверки работы кода может занимать значительное время, минуты и десятки минут. С другой стороны, результатом работы проверяемого кода может быть что-то сложное, например, множество записей в базе данных или вывод определённой непростой структуры. Тогда каждый запуск кода на проверку превращается в целое приключение.
Именно здесь на сцену выходит вариант «писать тесты до кода». У многих начинающих разработчиков, эта фраза вызывает ступор. Как можно писать тесты до того, как был написан код? Оказывается, можно и это даже приятно.
Допустим, мы хотим написать функцию, которая может повторять переданную строчку указанное количество раз:
Мы знаем что она принимает на вход, мы знаем, какой у неё должен быть выход (спасибо, что ты чистая). Можем ли мы уже написать тесты? Конечно!
Сколько займёт времени написать такой тест у опытного разработчика? Думаю секунд 15, именно столько времени мне понадобилось для кода выше. Зато теперь для проверки работы этого кода достаточно набрать phpunit в консоли.
У тестирования до написания кода есть ещё одно мощное преимущество. Оно заставляет программиста в первую очередь думать о дизайне своего решения, о том, как им будут пользоваться. А не о том, как красиво он реализует всё внутри. Грамотные интерфейсы – залог успеха.
В мире разработки подход, при котором тесты пишутся до кода, называется Test-Driven Development (TDD).
TDD по задумке изобретателя этой техники подразумевает, что вся разработка состоит из повторяющегося цикла, где на каждой итерации пишется тест, который не проходит, затем дописывается код, удовлетворяющий данному тесту. После этого всё повторяется. Так, шаг за шагом, строится приложение.
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.
Учебный пример разработки PHP-класса с использованием TDD
В данном посте приводится учебный пример разработки PHP-класса, который совершает запрос к Twitter API с целью выборки статусов пользователя по его никнейму. Кроме того, Twitter-класс кеширует полученные данные с использованием еще одного PHP-класса, который осуществляет простое кеширование данных в файлах.
Целью поста является закрепление собственных знаний, полученных в результате прочтения некоторых книг, статей, а также возможность получить комментарии от опытных TDD-практиков, с указанием на грубые ошибки в процессе разработки или в тестах.
Постановка задачи и требований
Итак, требуется разработать через тестирование класс на языке PHP, который способен совершать запросы к API Twitter и возвращать данные, полученные в ответ. Также необходимо предусмотреть, что Twitter-объект может использовать (а может и не использовать) кеширующий объект. Кеширующий объект должен сохранять данные в заранее заданной директории с заранее заданным временем жизни.
Так как разработка учебная, то условимся, что данные по статусам будут возвращаться сырые, в формате JSON.
Тестирование должно проводиться с использованием фреймворка PHPUnit.
Приступим к описанию требований к каждому классу, на основе которых будут составлены тесты.
Начинаем разрабатывать
Начнем разработку с класса FileCache, напишем первый тест:
Тест довольно простой, и он начинает успешно выполнятся после описания интерфейса:
… и после начального описания класса FileCache:
Для реализации дальнейших двух тестов необходимо запрограммировать методы подготовки фикстуры, а именно – создание и очищение каталога с кеш-файлами:
Данный тест осуществляет проверку, действительно ли кеш-файл появился в каталоге, который был задан при создании объекта FileCache.
Чтобы тест сработал успешно, я осуществил минимальные изменения класса FileCache:
Следующий тест, который реализует проверку времени жизни кеша:
Тест проверяет наличие кеш-данных до наступления их «протухания» и после. Реализация данного теста на мой взгляд неприемлема при разработке более крупного проекта, так как оператор sleep(3) задерживает выполнение теста на 3 секунды. Наиболее подходящий вариант – ручное изменение времени доступа к файлу.
Добавим в конструктор класса присвоение времени жизни кеша:
И добавим метод load:
На данном этапе уже можно провести рефакторинг кода с целью удаления дублирования кода в классе FileCache, а именно создание имени файла в методе load и save. Для этого добавим закрытый метод _createFilename. Данный отрезок кода уже был протестирован, поэтому можно не бояться, что закрытый метод будет неоттестирован (источник: blog.byndyu.ru, точный пост не помню, но все статьи одинаково полезны и интересны).
Для того, чтобы тест сработал, необходимо всего лишь добавить кусочек кода в метод load:
Итак, все тесты для класса FileCache работают, можно переходить к реализации класса Twitter.
Разрабатываем дальше
Начинаем с написания теста для первого же требования:
Данный тест проверит, что URL, по которому будет произведена выборка данных из Twitter API передается верно в объект Http-клиента. Так как тесты должны быть независимы, то я использую мок-объект для имитации Http-клиента. Я описываю, какой метод должен быть вызван у мок-объекта, сколько раз и с какими параметрами. Подробнее об этом можно прочитать в документации к PHPUnit.
Я сразу приведу еще один тест, который тестирует второе требование к классу Twitter:
Данный тест достаточно объемный. Здесь также используются мок-объекты. Кроме мок-объекта Http-клиента используется также и мок-объект кеш-класса, несмотря на то, что данный класс уже разработан (помним про независимость тестов). Тест проверяет, будет ли произведено обращение к HTTP, если данные уже есть в кэш. Кроме того сверяется корректность возвращенных данных.
Исходный код класса Twitter, который выполняет оба теста приведен далее:
Давайте TDD простое приложение на PHP
В этом уроке я представлю полный пример простого приложения, созданного строго с использованием TDD в PHP. Я проведу вас через каждый шаг, по одному, объясняя решения, которые я принял, чтобы выполнить задачу. Пример тесно следует правилам TDD: написание тестов, написание кода, рефакторинг.
Шаг 1 — Введение в TDD и PHPUnit
Разработка через тестирование (TDD)
TDD — это методика тестирования в первую очередь для разработки и проектирования программного обеспечения. Он почти всегда используется в гибких командах, являясь одним из основных инструментов гибкой разработки программного обеспечения. TDD был впервые определен и представлен профессиональному сообществу Кент Беком в 2002 году. С тех пор он стал общепринятой и рекомендуемой техникой в повседневном программировании.
TDD имеет три основных правила:
PHPUnit
PHPUnit — это инструмент, который позволяет программистам PHP выполнять модульное тестирование и практиковать разработку через тестирование. Это полная система модульного тестирования с поддержкой насмешек. Несмотря на то, что есть несколько альтернативных вариантов, PHPUnit является наиболее используемым и наиболее полным решением для PHP на сегодняшний день.
Чтобы установить PHPUnit, вы можете либо следовать предыдущему руководству в нашей сессии «TDD in PHP», либо использовать PEAR, как объяснено в официальной документации :
Некоторые дистрибутивы Linux предлагают phpunit в виде предварительно скомпилированного пакета, хотя я всегда рекомендую установку через PEAR, поскольку он гарантирует, что самая последняя и самая последняя версия установлена и используется.
NetBeans & PHPUnit
Если вы являетесь поклонником NetBeans, вы можете настроить его для работы с PHPUnit, выполнив следующие действия:
Если вы не используете IDE с поддержкой модульного тестирования, вы всегда можете запустить тест непосредственно из консоли:
Шаг 2 — проблема для решения
Нашей команде поручено реализовать функцию «перенос слов».
Давайте предположим, что мы являемся частью большой корпорации, которая имеет сложное приложение для разработки и поддержки. Нашей команде поручено реализовать функцию «перенос слов». Наши клиенты не хотят видеть горизонтальные полосы прокрутки, и это наша работа, чтобы соответствовать.
В этом случае нам нужно создать класс, способный форматировать произвольный бит текста, предоставляемый в качестве входных данных. Результатом должно быть слово, заключенное в указанное количество символов. Правила переноса слов должны соответствовать поведению других повседневных приложений, таких как текстовые редакторы, текстовые области веб-страниц и т. Д. Наш клиент не понимает всех правил переноса слов, но он знает, что хочет, и знает это. должен работать так же, как в других приложениях.
Шаг 3 — Планирование
TDD помогает вам достичь лучшего дизайна, но не устраняет необходимость в предварительном дизайне и мышлении.
После запуска TDD многие программисты забывают думать и планировать заранее. TDD большую часть времени помогает вам достичь лучшего дизайна, с меньшим количеством кода и проверенной функциональностью, но это не устраняет необходимость предварительного проектирования и человеческого мышления.
Каждый раз, когда вам нужно решить проблему, вы должны выделить время, чтобы подумать об этом, представить небольшой дизайн — ничего особенного — но достаточно, чтобы начать работу. Эта часть работы также помогает вам представить и угадать возможные сценарии логики приложения.
Давайте подумаем об основных правилах для функции переноса слов. Я полагаю, что нам будет передан какой-нибудь неупакованный текст Мы узнаем количество символов в строке и хотим, чтобы оно было перенесено. Итак, первое, что приходит мне в голову, это то, что, если текст содержит больше символов, чем число в одной строке, мы должны добавить новую строку вместо последнего пробела, который все еще находится в строке.
Хорошо, это подытожит поведение системы, но это слишком сложно для любого теста. Например, что делать, когда одно слово длиннее, чем количество символов в строке? Хммм … это похоже на крайний случай; мы не можем заменить пробел новой строкой, поскольку у нас нет пробелов в этой строке. Надо насильно завернуть слово, эффективно разделив его на две части.
Шаг 4 — Запуск проекта и создание первого теста
Архитектура чистого кода и разработка через тестирование в PHP
Перевод статьи Vitalij Mik Clean Code Architecture and Test Driven Development in PHP
Понятие «архитектура чистого кода» (Clean Code Architecture) ввел Роберт Мартин в блоге 8light. Смысл понятия в том, чтобы создавать архитектуру, которая не зависела бы от внешнего воздействия. Ваша бизнес-логика не должна быть объединена с фреймворком, базой данных или самим вебом. Подобная независимость даёт ряд преимуществ. К примеру, при разработке вы сможете откладывать какие-то технические решения, например выбор фреймворка, движка/поставщика БД. Также вы сможете легко переключаться между разными реализациями и сравнивать их. Но самое важное преимущество такого подхода — ваши тесты будут выполняться быстрее.
Просто подумайте об этом. Вы действительно хотите пройти роутинг, подгрузить абстрактный уровень базы данных или какое-нибудь ORM-колдовство? Или просто выполнить какой-то код, чтобы проверить (assert) те или иные результаты?
Я начал изучать такую архитектуру и практиковаться в ее создании из-за моего любимого фреймворка Kohana. Его основной разработчик однажды перестал поддерживать код, поэтому мои проекты не обновлялись и не получали патчи системы безопасности. А это означало, что мне понадобилось либо довериться версии, которая разрабатывается сообществом, либо переходить на новый фреймворк и переписывать проекты целиком.
Да, я мог бы выбрать другой фреймворк. Возможно, Symfony 1 или Zend 1. Но что бы я ни выбрал, с тех пор изменился бы и этот фреймворк. Они постоянно меняются и развиваются. Composer облегчает не только установку и замену пакетов, но и их исключение (в нём есть даже возможность помечать пакеты как исключённые), так что ошибиться довольно просто.
В этой публикации я покажу вам, как внедрить в PHP архитектуру чистого кода, которая позволит контролировать логику, не завися от внешних решений, но имея возможность их использовать. Мы изучим вопрос на примере создания простенького приложения гостевой книги.
На этой иллюстрации изображены разные слои приложения. Внутренние ничего не знают о внешних, при этом все они взаимодействуют друг с другом через интерфейсы.
Самое интересное — в правом нижнем углу: поток управления. Схема объясняет, как фреймворк взаимодействует с бизнес-логикой. Контроллер передаёт данные на порт ввода, информацию с которого обрабатывает интерактор, а результат передаётся на порт вывода, содержащий данные для презентера.
Начнём со слоя сценариев использования, поскольку здесь находится наша специфическая логика приложения. Внешние слои, включая контроллер, относятся к фреймворку.
Обратите внимание, что все описываемые далее этапы можно взять из репозитория. Они аккуратно разделены по шагам с помощью Git-тэгов. Просто скачайте нужный шаг, если хотите посмотреть, как это работает.
Первый тест
Обычно мы начинаем работу с пользовательского интерфейса. Что человек ожидает увидеть в гостевой книге? Наверное, форму ввода, записи других посетителей, возможно, навигационную панель с поиском по страницам записей. Если книга пуста, может отображаться сообщение «Записей нет».
В первом тесте нам нужно проверить (assert) пустой список записей:
Если следовать принципам разработки через тестирование (test-driven development, TDD) — красный цикл, зелёный цикл, цикл рефакторинга, — тест не будет пройден, поскольку классы не существуют. Для прохождения теста достаточно создать файлы классов, методы и свойства. Поскольку классы пусты, нам пока рано приступать к циклу рефакторинга.
Теперь нужно проверить отображение записей:
Наброски логики для useCase
Но сначала воспользуемся type hint’ами в качестве параметров и создадим интерфейсы:
Художники работают так же. Вместо рисования всей картины от начала и до конца они первым делом создают базовые формы и линии, чтобы представлять основу будущего изображения. А потом добавляют к формам всевозможные детали. Но вначале появляется эскиз.
Мы же вместо форм и линий используем, например, репозитории и фабрики. Репозиторий — это абстрактный уровень для получения данных из хранилища. Хранилищем может быть база данных, файл, внешний API и даже память.
Для просмотра записей в гостевой книге нам нужно найти эти записи в репозитории, конвертировать в виды (view) и вернуть.
Поскольку мы пока не знаем, какой формат нужно придать сущности, пропустим этот шаг.
Теперь отвечу на ваш возможный вопрос о фабриках. Создав новый экземпляр в цикле:
мы нарушим принцип инверсии зависимостей. И если потом в той же логике useCase нам понадобится ещё один объект вида (view object), то придётся переписывать код. А с помощью фабрики можно легко внедрять разные виды с разной логикой форматирования, при этом будет использоваться один и тот же useCase.
Реализация внешних зависимостей
EntryRepository выглядит так:
Как видите, тесты всё ещё не проходятся, так как нет реализации зависимости. Создадим несколько фальшивых объектов:
Теперь репозиторий выглядит так:
А фабрика видов — так:
Вы спросите, почему бы просто не использовать mocking-фреймворки для создания зависимостей? Тому есть две причины:
Рефакторинг теста
Тестовый класс выглядит так:
Независимость
Теперь мы можем, например, легко создать новые тесты с неверными сущностями, переместить репозиторий и фабрику в метод setup и прогнать тесты с настоящими реализациями.
Также мы можем внедрить в DI-контейнер готовый к использованию useCase и использовать его внутри фреймворка. При этом логика не будет зависеть от фреймворка.
Разбивка на страницы
Добавим в нашу гостевую книгу разбивку на страницы. Тест может выглядеть так:
В репозитории немного изменится метод findAllPaginated :
С помощью этого метода мы можем протестировать отображение следующих пяти записей:
Завершение
Этот туториал демонстрирует, как для любого нового проекта можно легко применять разработку через тестирование и архитектуру чистого кода. Главное преимущество такого подхода — полная независимость логики. Такой подход также позволяет использовать сторонние библиотеки.
Мой путь к тестам лежал в первую очередь через осознание того что мой код хрупок. Это не так просто, как может показаться на первый взгляд, — дело в том что в программистах очень живуч миф о «совершенности» их кода. Действительно, каждый раз когда в коде находится баг, программист в первую очередь думает о том где ошибся тестировщик, а уж потом, ищет проблему в программе. Если же в вас живет скептик, критикующий производимый вами код, у вас появляется сильное желание «успокоить его», подписав логгирование, обработчики ошибок и постоянно тестируя всё что пишите. В какой-то момент, когда бизнес начинает требовать от вас радикального поворота в логике вашего приложения, и вы понимаете что для поддержания целостности приложения и уверенности в том что оно всё еще работает «как надо», вам нужно проделать такой же объём работы, который вы уже проделали, и вы понимаете — будь у вас под рукой что-то типа тестов, это спасло бы ваши выходные. То есть понимание необходимости в модульных тестах приходит именно через сложности в разработке больших и сложных приложений. Я не напрасно написал «больших и сложных», — именно так. Для простых это может показаться лишним, так как если у вас нет большого опыта работы с тестами, вы будите писать медленнее, но если опыт есть, то и для небольших проектов модульные тесты это хорошая практика, так как «большие и сложные» проекты часто вырастают из небольших, и поэтому если ваш небольшой проект спроектирован должным образом и покрыт тестами это будет отличной площадкой для развития проекта и наращивания функциональности. Если же проект большой и несложный, для веб-приложений, это как правило, большое количество пользовательских форм, простая логика сохранения и отображения информации модульное тестирование также не будет «спасательным кругом», безусловно, тесты на основные классы должны быть написаны, но главной составляющей качества вашего продукта будет скорее всего функциональное тестирование интерфейсов. А вот в случае «больших и сложных» проектов тесты, неотъемлемая составляющая проекта. В противном случае «вертолет» может «не взлететь».
Итак, вы стоите на пороге нового проекта, понимание важности тестов у вас есть, и вы хотите знать, как «сделать правильно». Теперь самое время уделить вашему пониманию ООП, и умению использовать паттерны проектирования. Для того чтобы систему было легко тестировать, легко изменять функциональность, без постоянного переписывания тестов систему нужно проектировать хорошо. Если у вас в команде есть архитектор или разработчик с большим опытом, то вам крупно повезло — на множество «граблей», присущих новичкам, вы не наступите. То есть «грабли» конечно же будут, но это будут уже более «высокоуровневые» просчеты в архитектуре, а не глупые ошибки. Вы будете постигать нюансы построения бизнес логики для более простого тестирования, будете открывать секреты построения красивых интерфейсов для своих классов, предоставляя максимум свободы внутри классов и позволяя кардинально менять логику классов без переписывания тестов. Вы будете учиться оптимизировать тесты и вплетать их в ваш процесс разработки и на этом пути никогда не будет скучно или неинтересно. Модульное тестирование это увлекательный процесс и я гарантирую что если вы пойдете по нему, он отблагодарит вас новым уровнем качества вашего кода и ваших приложений. Пробуйте, интегрируйте запуск тестов в свою IDE и расскажите мне о ваших успехах.