Разработка через тестирование и модульное тестирование — это новейшие способы гарантировать, что код продолжает работать должным образом, несмотря на модификации и серьезные изменения. В этой статье вы узнаете, как проводить модульное тестирование вашего PHP-кода на уровнях модуля, базы данных и пользовательского интерфейса (UI).
Сейчас 3 часа ночи. Откуда мы знаем, что наш код все еще работает?
Веб-приложения работают круглосуточно, поэтому вопрос о том, работает ли моя программа, будет беспокоить меня по ночам. Модульное тестирование помогло мне обрести достаточную уверенность в своем коде, чтобы я мог спать спокойно.
Модульное тестирование — это платформа для написания тестовых примеров для вашего кода и автоматического запуска этих тестов. Разработка через тестирование — это подход к модульному тестированию, основанный на идее, что вы должны сначала написать тесты и проверить, могут ли эти тесты находить ошибки, и только потом приступать к написанию кода, который должен пройти эти тесты. Когда все тесты пройдены, разработанная нами функция завершена. Ценность этих модульных тестов в том, что мы можем запускать их в любое время — перед проверкой кода, после серьезных изменений или после развертывания в работающей системе.
Модульное тестирование PHP
Для PHP инфраструктурой модульного тестирования является PHPUnit2. Эту систему можно установить как модуль PEAR с помощью командной строки PEAR: % pear install PHPUnit2.
После установки платформы вы можете писать модульные тесты, создавая тестовые классы, производные от PHPUnit2_Framework_TestCase.
Модульное тестирование модуля
Я обнаружил, что лучше всего начинать модульное тестирование с модуля бизнес-логики приложения. Я использую простой пример: это функция, которая суммирует два числа. Чтобы начать тестирование, мы сначала напишем тестовый пример, как показано ниже.
Листинг 1. TestAdd.php
<?phprequire_once 'Add.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestAdd расширяет PHPUnit2_Framework_TestCase{ function test1() { $this->assertTrue( add( 1, 2 ) == 3 ); } function test2() { $this->assertTrue( add( 1, 1 ) == 2 }}?>
Этот класс TestAdd имеет два метода, оба из которых используют тестовый префикс. Каждый метод определяет тест, который может быть простым, как в листинге 1, или очень сложным. В этом случае мы просто утверждаем, что 1 плюс 2 равняется 3 в первом тесте, а 1 плюс 1 равняется 2 во втором тесте.
Система PHPUnit2 определяет метод AssertTrue(), который используется для проверки истинности значения условия, содержащегося в параметре. Затем мы написали модуль Add.php, который поначалу выдавал неверные результаты.
Листинг 2. Add.php
<?phpfunction add( $a, $b ) { return 0 }?>
Теперь при запуске модульных тестов оба теста завершаются неудачно.
Листинг 3. Сбои теста
% phpunit TestAdd.phpPHPUnit 2.2.1 от Sebastian Bergmann.FFTime: 0.0031270980834961 Было 2 сбоя: 1) test1(TestAdd)2) test2(TestAdd)FAILURES!!! Проведено тестов: 2, Сбоев: 2, Ошибок: 0, неполных тестов: 0.
Теперь я знаю, что оба теста работают нормально. Таким образом, функцию add() можно изменить, чтобы она действительно выполняла реальную задачу.
Оба теста теперь пройдены.
<?phpfunction add( $a, $b ) { return $a+$b; }?>
Листинг 4. Тест пройден
% phpunit TestAdd.phpPHPUnit 2.2.1 от Sebastian Bergmann...Time: 0.0023679733276367OK (2 теста)%
хотя Этот пример разработки через тестирование очень прост, но мы можем понять его идеи. Сначала мы создали тестовый пример и имели достаточно кода для запуска теста, но результат оказался неверным. Затем мы проверяем, что тест действительно не пройден, а затем реализуем реальный код, чтобы тест прошел.
Я обнаружил, что по мере реализации кода я продолжаю добавлять код до тех пор, пока не получу полный тест, охватывающий все пути кода. В конце этой статьи вы найдете несколько советов о том, какие тесты писать и как их писать.
Тестирование базы данных
После тестирования модуля можно провести тестирование доступа к базе данных. Тестирование доступа к базе данных поднимает две интересные проблемы. Во-первых, перед каждым тестом мы должны восстановить базу данных в некоторую известную точку. Во-вторых, имейте в виду, что это восстановление может привести к повреждению существующей базы данных, поэтому мы должны тестировать непроизводственную базу данных или быть осторожными, чтобы не повлиять на содержимое существующей базы данных при написании тестовых примеров.
Модульное тестирование базы данных начинается с базы данных. Чтобы проиллюстрировать эту проблему, нам нужно использовать следующий простой шаблон.
Листинг 5. Schema.sql
DROP TABLE IF EXISTSauthors;CREATE TABLEauthors (id MEDIUMINT NOT NULL AUTO_INCREMENT, имя TEXT NOT NULL, PRIMARY KEY (id));
Листинг 5 представляет собой таблицу авторов, и каждая запись имеет связанный идентификатор.
Далее вы можете написать тестовые примеры.
Листинг 6. TestAuthors.php
<?phprequire_once 'dblib.php';require_once 'PHPUnit2/Framework/TestCase.php';класс TestAuthors расширяет PHPUnit2_Framework_TestCase{ function test_delete_all() { $this->assertTrue( Authors::delete_all() ); } function test_insert() { $this->assertTrue( Authors::delete_all() ); $this->assertTrue( Authors::insert( 'Jack' ) } function test_insert_and_get() { $this->assertTrue( Authors ::delete_all() ); $this->assertTrue( Authors::insert( 'Jack' ) ); $this->assertTrue( Authors::insert( 'Joe' ) ); $found = Authors::get_all() ); ; $this->assertTrue( $found != null ); $this->assertTrue( count( $found ) == 2 }}?>
Этот набор тестов охватывает удаление авторов из таблицы и вставку авторов в таблицу. А также такие функции, как вставка автора при проверке его существования. Это накопительный тест, который я считаю очень полезным для поиска ошибок. Наблюдая за тем, какие тесты работают, а какие нет, вы сможете быстро выяснить, что происходит не так, а затем глубже понять различия.
Ниже показана версия кода доступа к базе данных PHP dblib.php, которая первоначально вызвала сбой.
Листинг 7. dblib.php
<?phprequire_once('DB.php'); class Authors{ public static function get_db() { $dsn = 'mysql://root:password@localhost/unitdb'; $db =& DB: :Connect($dsn, array()); if (PEAR::isError($db)) { die($db->getMessage()); return $db; } public static function delete_all() { return false; } public static function Insert($name) { return false; } public static function get_all() { return null }}?>
Выполнение модульных тестов кода в листинге 8 покажет, что все три теста не пройдены:
Листинг 8. dblib; php
% phpunit TestAuthors.phpPHPUnit 2.2.1 от Sebastian Bergmann.FFFTime: 0.007500171661377 Было 3 сбоя: 1) test_delete_all(TestAuthors)2) test_insert(TestAuthors)3) test_insert_and_get(TestAuthors)ОШИБКИ!!!Тестов выполнено: 3, сбоев: 3, Ошибок: 0, Незавершенных тестов: 0,%
Теперь мы можем начать добавлять код для правильного доступа к базе данных — метод за методом — до тех пор, пока не пройдут все 3 теста. Окончательная версия кода dblib.php показана ниже.
Листинг 9. Завершение dblib.php
<?phprequire_once('DB.php');class Authors{ public static function get_db() { $dsn = 'mysql://root:password@localhost/unitdb'; $db =& DB; ::Connect($dsn, array()); if (PEAR::isError($db)) { die($db->getMessage() } return $db } public static function delete_all() { $ db = Авторы::get_db(); $sth = $db->prepare('УДАЛЕНИЕ ИЗ авторов'); $db->execute($sth); return true; } public static function Insert($name) {$db = Авторы::get_db(); $sth = $db->prepare( 'ВСТАВИТЬ В ЗНАЧЕНИЯ авторов (null,?)' ); $db->execute($sth, array($name) } public; статическая функция get_all() { $db = Authors::get_db(); $res = $db->query( "SELECT * FROMauthors"); $rows = array(); while($res->fetchInto($row). ) ) { $rows []= $row; } return $rows; }}?>
HTML-тестирование
Следующим шагом в тестировании всего PHP-приложения является тестирование внешнего интерфейса языка гипертекстовой разметки (HTML). Для выполнения этого теста нам нужна веб-страница, подобная приведенной ниже.
Листинг 10. TestPage.php
<?phprequire_once 'HTTP/Client.php';require_once 'PHPUnit2/Framework/TestCase.php';класс TestPage расширяет PHPUnit2_Framework_TestCase{ function get_page( $url ) { $client = new HTTP_Client(); ->get($url); $resp = $client->currentResponse(); return $resp['body'] } function test_get() { $page = TestPage::get_page( 'http://localhost/unit); /add.php'); $this->assertTrue(strlen($page) > 0); $this->assertTrue(preg_match( '/<html>/', $page) == 1 } function test_add( ) { $page = TestPage::get_page( 'http://localhost/unit/add.php?a=10&b=20'); $this->assertTrue(strlen($page) > 0 ); AssertTrue( preg_match( '/<html>/', $page) == 1 ; preg_match( '/<span id="result">(.*?)</span>/', $page, $out); ); $this->assertTrue( $out[1]=='30' ); }}?>
В этом тесте используется модуль HTTP-клиента, предоставленный PEAR. Я считаю, что это немного проще, чем встроенная библиотека URL-адресов клиента PHP (CURL), но последнюю также можно использовать.
Существует тест, который проверяет возвращаемую страницу и определяет, содержит ли она HTML. Второй тест запрашивает сумму 10 и 20, помещая значение в запрошенный URL-адрес, а затем проверяет результат на возвращаемой странице.
Код этой страницы показан ниже.
Листинг 11. TestPage.php
<html><body><form><input type="text" name="a" value="<?php echo($_REQUEST['a']); ?>" /> + <input type="text" name="b" value="<?php echo($_REQUEST['b']); ?>" /> =<span id="result"><?php echo($_REQUEST ['a']+$_REQUEST['b']); ?></span><br/><input type="submit" value="Добавить" /></form></body></html >
Эта страница довольно проста. В двух полях ввода отображаются текущие значения, указанные в запросе. Диапазон результатов показывает сумму этих двух значений. В разметке отмечаются все различия: она невидима пользователю, но видна юнит-тестам. Таким образом, модульным тестам не нужна сложная логика, чтобы найти это значение. Вместо этого он извлекает значение определенного тега. Таким образом, при изменении интерфейса, пока существует диапазон, тест будет пройден.
Как и раньше, сначала напишите тестовый пример, а затем создайте неудачную версию страницы. Мы проверяем на наличие ошибок, а затем изменяем содержимое страницы, чтобы она работала. Результаты следующие:
Листинг 12. Сбой теста и последующее изменение страницы
% phpunit TestPage.phpPHPUnit 2.2.1 от Sebastian Bergmann...Time: 0.25711488723755OK (2 теста)%
Оба теста могут быть пройдены, что означает, что тест пройден код, он работает нормально.
При запуске тестов этого кода все тесты выполняются без проблем, поэтому мы знаем, что наш код работает правильно.
Но у тестирования HTML-интерфейса есть недостаток: JavaScript. Клиентский код протокола передачи гипертекста (HTTP) извлекает страницу, но не выполняет JavaScript. Итак, если у нас много кода на JavaScript, нам нужно создать модульные тесты на уровне пользовательского агента. Лучшим способом достижения этой функциональности, который я нашел, является использование функций уровня автоматизации, встроенных в Microsoft® Internet Explorer®. С помощью сценариев Microsoft Windows®, написанных на PHP, вы можете использовать интерфейс модели компонентных объектов (COM) для управления Internet Explorer для навигации между страницами, а затем использовать методы объектной модели документа (DOM) для поиска страниц после выполнения определенных действий пользователя. .
Это единственный известный мне способ модульного тестирования внешнего кода JavaScript. Я признаю, что его нелегко писать и поддерживать, и эти тесты легко ломаются, когда в страницу вносятся даже небольшие изменения.
Какие тесты писать и как их писать
. При написании тестов я предпочитаю рассматривать следующие сценарии:
Все положительные тесты.
Этот набор тестов гарантирует, что все работает так, как мы ожидаем.
Все отрицательные тесты
используют эти тесты один за другим, чтобы гарантировать проверку каждого сбоя или аномалии.
Тестирование положительной последовательности
Этот набор тестов гарантирует, что вызовы в правильной последовательности работают так, как мы ожидаем.
Тестирование отрицательной последовательности.
Этот набор тестов гарантирует, что вызовы завершатся неудачей, если они выполняются в неправильном порядке.
Нагрузочное тестирование.
При необходимости можно выполнить небольшой набор тестов, чтобы определить, соответствует ли производительность этих тестов нашим ожиданиям. Например, 2000 вызовов должны завершиться в течение 2 секунд.
Тесты ресурсов.
Эти тесты гарантируют, что интерфейс прикладного программирования (API) правильно распределяет и освобождает ресурсы — например, вызывает API открытия, записи и закрытия файлов несколько раз подряд, чтобы убедиться, что ни один файл еще не открыт.
Тесты обратного вызова
Для API, имеющих методы обратного вызова, эти тесты гарантируют, что код работает нормально, если функция обратного вызова не определена. Кроме того, эти тесты также могут гарантировать, что код по-прежнему может работать нормально, если определены функции обратного вызова, но эти функции обратного вызова работают неправильно или генерируют исключения.
Вот некоторые мысли о модульном тестировании. У меня есть несколько советов по написанию модульных тестов:
Не используйте случайные данные
.Хотя генерация случайных данных в интерфейсе может показаться хорошей идеей, мы хотим избегать этого, потому что эти данные могут стать очень трудными для отладки. Если данные генерируются случайным образом при каждом вызове, может случиться так, что ошибка возникнет в одном тесте, но не произойдет в другом тесте. Если для вашего теста требуются случайные данные, вы можете сгенерировать их в файле и использовать этот файл при каждом его запуске. При таком подходе мы получаем некоторые «зашумленные» данные, но все равно можем отлаживать ошибки.
Групповое тестирование
Мы можем легко накопить тысячи тестов, выполнение которых занимает несколько часов. В этом нет ничего плохого, но группировка этих тестов позволяет нам быстро запустить набор тестов и проверить наличие серьезных проблем, а затем запустить полный набор тестов ночью.
Написание надежных API и надежных тестов
Важно писать API и тесты так, чтобы их нельзя было легко сломать при добавлении новых функций или изменении существующих функций. Не существует универсального волшебного средства, но практическое правило заключается в том, что тесты, которые «колеблются» (иногда терпят неудачу, иногда успешны, снова и снова), должны быть быстро отброшены.
Заключение
Модульное тестирование имеет большое значение для инженеров. Они являются основой для гибкого процесса разработки (в котором большое внимание уделяется кодированию, поскольку документация требует некоторых доказательств того, что код работает в соответствии со спецификациями). Модульные тесты предоставляют это доказательство. Процесс начинается с модульных тестов, которые определяют функциональные возможности, которые код должен реализовать, но в настоящее время не реализует. Поэтому все тесты изначально пройдут неудачно. Затем, когда код почти готов, тесты пройдены. Когда все тесты пройдены, код становится очень полным.
Я никогда не писал большой код и не изменял большой или сложный блок кода без использования модульных тестов. Обычно я пишу модульные тесты для существующего кода перед его модификацией, просто чтобы убедиться, что я знаю, что я нарушаю (или не нарушаю), когда изменяю код. Это дает мне большую уверенность в том, что код, который я предоставляю своим клиентам, работает правильно — даже в 3 часа ночи.