Введение
Главная задача профессионального php-разработчика - cоздание в максимально короткий срок программного обеспечения, полностью удовлетворяющего заказчика. Скрипты должны делать именно то, чего от них ожидают. Самый простой пример - форма обратной связи, которая по нажатию на кнопку при условии корректного заполнения полей отправляет почту или записывает данные в таблицу БД. Если она этого не сделала, совершенно очевидно, что в программу закралась ошибка.
Итак, тестирование - деятельность, направленная на выявление такого рода несоответствий между ожидаемым и действительным. Выявляя несоответствия, некорректное поведение на стадии разработки, разработчик сознательно и планомерно уменьшает вероятность того, что с этим придется столкнуться пользователю. Тестировать программы (в частности php-скрипты) вполне можно и вручную. На примере формы обратной связи, понятно, что протестировать ее очень просто. Запустить браузер, зайти на нужный url, заполнить поля, нажать на "Ок", потом зайти в базу и проверить, появилась ли там соотвeтствующая запись. Прекрасно, но после любого изменения скрипта нельзя быть уверенным в его работоспособности. Можно понадеяться на авось и не проверять, работает ли скрипт после очередной модификации или нет, но помните, что если его не протестируете вы, это сделает заказчик во время сдачи проекта или конечный пользователь во время эксплуатации. В первом случае это грозит вам финансовой нестабильностью, а во втором пострадает ваш имидж. Для того, чтобы не тестировать вручную (это долгий, сложный и требующий особой усидчивости процесс), можно написать программу, тестирующую другие программы, или создать автоматизированные тесты.
В любой современной методологии разработки ПО тестирование является неотъемлемой частью процесса, а значимость его написания не ниже значимости написания кода. Профессиональный разработчик обязан создавать наборы различного вида автоматизированных тестов (подробнее о видах тестов, правилах их именования и т.д. можно прочитать в статье Кирилла Максимова "Организация и именование автоматизированных тестов").
Несмотря на очевидную полезность и акцент методологий на процессе тестирования, личный опыт авторов свидетельствует о следующем разбросе мнений web-программистов насчет автоматизированного тестирования собственных скриптов:
* "Никто не пишет, почему я должен писать"
* "Мне не выделяют времени специально для написания тестов, хотя это было бы полезно"
* "Я давно пишу тесты и не понимаю, как можно без этого жить"
Автоматизированные тесты и web-программирование
Будни основной массы web-разработчиков - выбор хорошего сервера для размещения сайта. Трудно найти хостинг-провайдера (php+mysql за разумные деньги), который устроил бы сразу на 100%, а пeреезд на новый сервер даже для интерпретируемого языка, коим является мой горячо любимый PHP, может стать "фатальным". Даже в документации по Java - языке, изначально ориентированном на кроссплатформенность, написано, что цена переносимости никогда не равна нулю. Эти люди знают, что говорят. После переезда в прекрасно отлаженных и стабильно работающих ранее скриптах могут появиться warning'и, а какие-то программы перестанут работать совсем: поменялись пути, библиотеки, версия самого php.
В итоге, имея набор тестов, переехать гораздо проще. Следует переписать скрипты, запустить набор тестов, посмотреть те тесты, которые завершились неудачно, разобраться, поправить, запустить набор тестов еще раз. Все. Вместо того, чтобы оставить "полуживой" проект на новом хостинге, "поверхностно" прокликав в браузере по ссылкам, мы получаем стабильно работающий сайт на новом сервере без особой головной боли. Автоматизированное тестирование в конечном итоге повышает качество кода, и, учитывая специфику web-программирования, упрощает процесс переезда с сервера на сервер.
Вслед за решением тестировать, приходит необходимость как-то организовать этот процесс. Например, выделить для тестовых скриптов отдельную директорию, создать программу, которая запустит все скрипты и в зависимости от их исхода напишет "OK" или "Error". Вполне возможно проделать это "с нуля", но есть готовые тестовые инфраструктуры (вспомогательные программы), которые объединяют группы тестов "по темам", единообразно их сохраняют, запускают и выводят результаты.
Cоздание тестов в phpUnit
В основном из-за увлеченности методологией XP авторы знакомы с тестовой инфраструктурой JUnit, соответственно, для php их выбор пал на phpUnit, как на созданный энтузиастами порт своего "старшего брата". На сегодняшний день существует несколько различных версий порта JUnit на php. Как минимум 3 варианта c названием phpUnit и порт php_simpletest. Регулярно обновляется версия PEAR::PHPUnit (Sebastian Bergmann). Php_simpletest находится в состоянии первой беты, другие phpUnit'ы обновлялись год назад (phpUnit project) и два года назад (phpunit-1.0.0) соответственно. Все рассмотренные ниже примеры модульных тестов написаны с использованием PEAR::PHPUnit.
Создадим набор тестов для класса Message, который форматирует и проверяет сообщение для последующей отправки его по e-mail.
Конструктор класса получает 3 параметра (имя отправителя, его e-mail aдрес и тело сообщения). Метод format_message() форматирует сообщение перед отправкой, а is_valid() проверяет возможность отправки сообщения (заполнены ли поля, и корректно ли введен е-mail). Исходный код класса Message - здесь
"Сердцем" тестирования является класс PHPUnit, который запускает TestSuite (набор тестов) и возвращает объект TestResult (результат теста).
Для того, чтобы написать минимальный набор тестов, используя phpUnit, необходимо:
1. Подключить библиотеку PHPUnit.php
2. Создать подкласс базового класса TestCase
3. Добавить в него произвольное количество тестирующих методов, названия которых начинаются с "test". В нашем случае это "test_empty_input","test_email_invalid","test_valid_input". В них будут вызываться методы тeстируемого класса Message. На вход будут подаваться заранее известные параметры, а результат сравнивается с эталонным посредством семейства функций Assert, унаследованной нашим тестовым классом от TestCase (метод assertEquals проверяет ожидаемый и реально полученный результат на равенство, assertTrue проверяет, имеет ли поданный параметр значение "true" и т. д. )
4. Cоздать класс PHPUnit_TestSuite, передав ему в качестве параметра название класса с набором тестов
5. Запустить набор тестов и вывести результат
Создадим набор тестов для класса, следуя приведенной инструкции.
Мы протестируем следующие аспекты работы класса Message: его реакцию на пустое сообщение, и некорректный e-mail, а также сравним отформатированное объектом класса сообщение с эталонным.
Создаем сообщение, подаем на вход конструктора неверный e-mail, ожидаем, что метод класса is_valid(), ответственный за его проверку, возвратит 'false'.
<?php
function test_email_invalid() {
$m = new Message('name', 'invalid', 'body');
/* ожидаем, что m->is_valid() возвращает false */
$this->assertFalse($m->is_valid());
}
Создаем сообщение, подаем на вход конструктора пустые строки, ожидаем, что метод класса is_valid(), так же, как и в прошлый раз, вернет false.
<?php
function test_empty_input() {
$m = new Message('', '', '');
/* ожидаем, что m->is_valid() возвращает false */
$this->assertFalse($m->is_valid());
}
И, наконец, подаем на вход корректно сформированное сообщение и ожидаем, что класс его правильно сформатирует
<?php
function test_valid_input() {
$m = new Message('name', '
[email protected]', 'body');
$this->assertTrue($m->is_valid());
$valid_string = <<<EOL
from: name (
[email protected])
body
EOL;
/* ожидаем, что эталонная строка и результат работы as_string() совпадут */
$this->assertEquals($valid_string, $m->as_string());
}
}
Запускаем набор тестов (полная версия testmessage.php - здесь) и получаем следующий результат:
<?php
TestCase messagetest->test_email_invalid() passed
TestCase messagetest->test_empty_input() passed
TestCase messagetest->test_valid_input() passed
Все тесты завершились успешно. Теперь можно продолжить наращивание функциональности класса Message, будучи уверенным, что после очередной модификации он корректно работает (конечно, при условии, что сработали все тесты).
Разработка класса Message и разработка Message c набором тестов не очень сильно различаются по времени, но во втором случае мы имеем мощную "поддержку" на будущее, над каждой функцией мы размышляем как минимум дважды, и позже можем легко определить работоспособность скрипта на новой программно-аппаратной конфигурации.
Приложение: исходный код message.inc.php
<?php
class Message {
var $email;
var $name;
var $body;
var $message;
function Message($n, $from, $b) {
$this->email = $from;
$this->name = $n;
$this->body = $b;
$this->message = null;
}
function is_valid() {
if (strpos($this->email, '@')===false) return false;
if (!strlen($this->name)) return false;
if (!strlen($this->body)) return false;
return true;
}
function as_string() {
if ($this->message==null) $this->format_message();
return $this->message;
}
function format_message() {
$this->message = 'from: '.$this->name;
$this->message .= ' ('.$this->email.")nn";
$this->message .= $this->body;
}
}
?>
Приложение: исходный код testmessage.php.txt
<?php
require_once 'PHPUnit.php';
/* подключаем phpUnit и файл с тестируемым классом */
require_once('class_message.inc.php');
/* наследуем от PHPUnit_TestCase и добавляем тестирующие методы test<...> */
class MessageTest extends PHPUnit_TestCase {
function test_email_invalid() {
$m = new Message('name', 'invalid', 'body');
/* ожидаем, что m->is_valid() возвращает false */
$this->assertFalse($m->is_valid());
}
function test_empty_input() {
$m = new Message('', '', '');
/* ожидаем, что m->is_valid() возвращает false */
$this->assertFalse($m->is_valid());
}
function test_valid_input() {
$m = new Message('name', '
[email protected]', 'body');
$this->assertTrue($m->is_valid());
$valid_string = <<<EOL
from: name (
[email protected])
body
EOL;
/* ожидаем, что эталонная строка и результат работы as_string() совпадут */
$this->assertEquals($valid_string, $m->as_string());
}
}
/* запускаем набор тестов и выводим результат */
$suite = new PHPUnit_TestSuite('MessageTest');
$result = PHPUnit::run($suite);
echo $result->toHTML();
?>