Изменения носят глобальный характер и без практически полного переписывания кода нам не обойтись. Двигаться следует небольшими, но решительными шагами. План таков:
-
используем WACT (версия 0.2alpha,
http://wact.sourceforge.net) в качестве шаблонной системы и в качестве DBAL(Database Abstraction Layer)
-
Все внешние библиотеки будем хранить в директории /external нашего приложения.
Наша задача - обезопасить функциональность приложения от будущих изменений, то есть мы должны гарантировать, что все наши будущие рефакторинги не приведут к тому, что мы потеряем часть его фукциональности приложения. Для этого существуют функциональные тесты. Для создания функциональных тестов мы будем использовать входящую в SimpleTest подсистему WebTester. Подробную информацию можно получить:
Работа с библиотекой SimpleTest при web тестировании определенным образом напоминает работу непосредственно с браузером. WebTester по сути предоставляет удобные методы, эмулирующие браузер, а именно:
получение страниц по адресу
навигирование по ссылкам и кнопкам
заполнение и отправление форм
организация непосредственных GET, POST, HEAD запросов
эмуляция фреймов
формирование
HTTP заголовков
установка и модификация cookies
Кроме этого WebTester позволяет посмотреть на запрос «изнутри»:
И конечно же, самой главной особенностью WebTester является способность проверить полученные в процессе браузинга результаты, вот лишь некоторые возможности:
сравнить контент страницы на предмет совпадения с некоторым регулярным выражением
проверить содержимое <title> тега
проверить наличие ссылок и их содержимого
удостовериться в правильности содержимого полей формы
проверить cookie на содержимое
-
Для всего вышеописанного WebTester предоставляет исключительно чистые и понятные интерфейсы, превращающие работу с ним в удовольствие, и скоро в этом убедитесь.
Для начала создадим директорию /tests, в которой будут располагаться все тесты для нашего приложения. В этой директории создадим файл runtests.php следующего содержания:
<?php
require_once(dirname(__FILE__) . '/setup.php');
class AllTests extends GroupTest {
function AllTests() {
$this->GroupTest('All tests for feedback project');
$this->addTestFile('acceptance_tests.php');
}
}
$test =& new AllTests();
if (SimpleReporter::inCli()) {
exit ($test->run(new TextReporter()) ? 0 : 1);
}
$test->run(new HtmlReporter());
?>
Этот скрипт будет точкой входа для всех тестов, причем его можно запускать как из консоли, так и из браузера. Для работы этого скрипта нам также потребуются файлы setup.php и acceptance_tests.php. В setup.php мы будем хранить глобальные настройки для всех тестов. Пока мы в нем только подключаем библиотеку SimpleTest и определяем адрес web хоста с приложением:
<?php
define('SIMPLE_TEST', dirname(__FILE__) . '/../external/simpletest/');
define('FEEDBACK_PROJECT_HOST', 'http://localhost/feedback/');
if (!file_exists(SIMPLE_TEST . '/browser.php')) {
die ('Make sure the SIMPLE_TEST constant is set correctly in this file(' . SIMPLE_TEST . ')');
}
require_once(SIMPLE_TEST . '/web_tester.php');
require_once(SIMPLE_TEST . '/reporter.php');
require_once(SIMPLE_TEST . '/unit_tester.php');
require_once(SIMPLE_TEST . '/mock_objects.php');
?>
В acceptance_tests.php будут располагаться все функциональные тесты для приложения. Не долго раздумывая, поместим в него самый первый тест:
<?php
class AcceptanceTestOfFeedbackProject extends WebTestCase {
function testOfIndexPage() {
$this->get(FEEDBACK_PROJECT_HOST);
$this->assertWantedPattern('/Обратная связь/');
}
?>
Суть данного теста сводится к посещения главной страницы нашего приложения и удостоверению, что страница содержит текст «Обратная связь». Допустим, что этот тест сработал, теперь можно перейти к более сложному тесту, целью которого будет проверка правильности отправки формы. Однако мы помним, что приложение сейчас работает с продукционной базой данных, что крайне опасно!!! Нам необходимо некоторым образом заставить приложение работать с другими настройками БД - тестовыми. К счастью, оригинальные разработчики решили хранить конфигурационные данные в отдельном файле db.php, который подключается в index.php. Мы можем на время тестов заменять db.php другим файлом, в котором находятся тестовые настройки. Но как это сделать лучше всего?
Каждый тестовый прецедент должен в идеале быть независимым, атомарным и выполняться в «чистой» среде, в которой не осталось мусора от выполнения предыдущих прецедентов.
SimpleTest позволяет подготовить некоторую окружающую среду для каждого тестового прецедента. Такая окружающая среда называется фикстурой(fixture). Сделать это можно при помощи методов setUp() и tearDown(). Эти методы вызываются соответсвенно до и после каждого тестового метода, что дает возможность разработчику произвести определенные подготавливающие мероприятия(очистка/заполнение БД, удаление временных файлов и проч).
Как упоминалось ранее, сделам так, чтобы на время тестов настройки базы данных подменивались тестовыми значениями. Для этого в директории tests создадим файл db.php - аналог того, который находится в корне приложения.
<?php
$db_host = 'localhost';
$db_name = 'feedback-web-tests';
$db_user = 'root';
$db_password = 'test';
?>
Теперь напишем фикстуру, подменяющую эти файлы перед каждым тестовым прецедентом. Также заставим фикстуру полностью очищать таблицу feedback, чтобы каждый тестовый прецедент имел «чистую» окружающую среду.
<?php
class AcceptanceTestOfFeedbackProject extends WebTestCase {
[...]
function setUp() {
$this->_switchToWebTestingDb();
}
function tearDown() {
$this->_switchToProductionDb();
}
function _switchToWebTestingDb() {
$project_dir = dirname(__FILE__) . '/../';
$tests_dir = dirname(__FILE__) . '/';
include($tests_dir . 'db.php');
$conn = mysql_connect($db_host, $db_user, $db_password);
mysql_select_db($db_name, $conn);
mysql_query('DELETE FROM feedback', $conn);
rename($project_dir . 'db.php', $project_dir . 'db.php~');
copy($tests_dir . 'db.php', $project_dir . 'db.php');
}
function _switchToProductionDb() {
$project_dir = dirname(__FILE__) . '/../';
unlink($project_dir . 'db.php');
rename($project_dir . 'db.php~', $project_dir . 'db.php');
}
}
?>
Теперь когда наша продукционная база данных защищена от фатальных последствий можно приступать к тестирования отправки новых сообщений с формы.
Итак, тестовый случай будет выглядеть так:
<?php
class AcceptanceTestOfFeedbackProject extends WebTestCase {
[...]
function testOfSimpleSubmitFeedback() {
$this->_addFeedback($name = 'Bobby',
$email = 'email@dot.com',
$message = "This a message with `non-escaped characters`");
$this->assertWantedPattern('/' . preg_quote($email) . '.*' .
$name . '.*' .
$message . '/s');
}
function _addFeedback($name, $email, $message) {
$this->get(FEEDBACK_PROJECT_HOST);
$this->setField('name', $name);
$this->setField('email', $email);
$this->setField('message', $message);
$this->clickSubmitByName('submit');
sleep(1);
}
}
?>
Как вы успели заметить, мы также добавили внутренний метод, _addFeedback, который заполняет поля формы и отсылает ее. Этот метод окажется весьма кстати в последующих тестах. Постоянный рефакторинг тестов - не менее важная задача, чем рефакторинг тестируемого кода. Чтобы избежать ситуации когда у нас может быть несколько сообщений, пришедших в одно и то же время, мы принуждаем PHP «засыпать» на 1 секунду после добавления каждого сообщения.
Этот тест также успешно срабатывает.
Добавим метод, проверяющий, что при выводе сообщения обрабатываюся на предмет небезопасных символов и тегов.
<?php
class AcceptanceTestOfFeedbackProject extends WebTestCase {
[...]
function testOfEscapingUserInput() {
$this->_addFeedback('<script>',
'<br>',
'"\'');
$this->assertWantedPattern("/<br>.*<script>.*\\\"\\\'/s");
}
}
?>
Теперь напишем тест, проверяющий правильность работы пейджера при добавлении нескольких сообщений:
<?php
class AcceptanceTestOfFeedbackProject extends WebTestCase {
[...]
function testOfPager() {
$this->get(FEEDBACK_PROJECT_HOST);
$this->assertNoLink("<");
$this->assertNoLink(">");
for($i=1; $i<8; $i++) {
$this->_addFeedback('Robot' . $i,
'robot' . $i . '@usrobotics.com',
'Hello i am Robot' . $i);
}
$this->get(FEEDBACK_PROJECT_HOST);
$this->assertWantedPattern('/Robot7.*Robot6.*Robot5/s');
$this->assertNoLink("<");
$this->assertLink(">");
$this->clickLink(">");
$this->assertWantedPattern('/Robot4.*Robot3.*Robot2/s');
$this->assertLink("<");
$this->assertLink(">");
$this->clickLink(">");
$this->assertWantedPattern('/Robot1/');
$this->assertLink("<");
$this->assertNoLink(">");
$this->clickLink("<");
$this->assertWantedPattern('/Robot4.*Robot3.*Robot2/s');
$this->assertLink("<");
$this->assertLink(">");
$this->clickLink("<");
$this->assertWantedPattern('/Robot7.*Robot6.*Robot5/s');
$this->assertNoLink("<");
$this->assertLink(">");
}
}
?>
Пейджер выводит по 3 сообщения, поэтому мы добавляем в тесте 8 сообщений, чтобы проверить граничные ситуации. В этом тесте мы также воспользовались методом clickLink класса WebTestCase, который позволяет проэмулировать навигирование пользователся по ссылкам.
Эти тесты покрывают весь функционал приложения, поэтому, убедившись в том, что все работает, мы приступаем к долгожданному рефакторингу приложения.
Далее - Шаг второй - отделяем бизнес логику от презентационной логики .