11 октября 2007 в 0:00
Любое программное обеспечение перед выпуском для массового использования должно быть тщательно оттестировано.
В таких языках программирования, как Java или С++, часть тестирования кода берет на себя компилятор, проверяя, определены ли все переменные, и следя за типами дынных. В языке программирования РНР, который, несомненно можно считать самым популярным средством разработки web-приложений, компилятор к коду относится весьма лояльно. Так, например, любую переменную можно определить где угодно и когда угодно. Нет необходимости явно указывать тип переменной, компилятор все сделает сам. Это нельзя считать ни плюсом, ни минусом, однако из-за этого некоторые разработчики допускают множество ошибок.
Для РНР, как и для другого языка программирования, может быть применен метод разработки через тестирование (TDD) – Test Driven Development. Тестирование – неотъемлемая часть написания ПО, которая помогает обнаружить ошибки и уязвимости в самом начале разработки. Ведь программист не всегда в состоянии предусмотреть все варианты типов входных данных и дальнейшей работы с ними.
Тестирование кода коренным образом меняет отношение к разработке. Для того чтобы привыкнуть тестировать свои приложения, может уйти от нескольких месяцев до года. Тестирования бывает модульным – в таком случае тестируется исходный код программы, а бывает приемочным – тестируется интерфейс программы или попросту соответствие приложения требованиям заказчика. Хорошо разработанные тесты могут вполне заметить документацию к скриптам, и другим программистам, которые впоследствии могут работать с вашим кодом, это намного облегчит понимание и доработку программы. Конечно, для этого тесты должны быть небольшими и понятными.
Иногда тесты даже могут заменить техническое задание: вы пишите тестовый код и потом по нему разрабатывается реализация. Если тесты срабатывают, задание можно считать выполненным.
В качестве средства модульного тестирования приложений, написанных на РНР, мы используем средство автоматического тестирования Simple Test (существуют и другие, но Simple Test весьма прост в освоении и распространяется как Open Source). Скачать последнюю версию Simple Test, а также ознакомиться с тем, как установить этот пакет тестов, можно на сайте: http://simpletest.sf.net . В работе мы используем даже два средства - Simple Test и WACT.
Важным моментом в процессе внедрения тестирования является рефакторинг – улучшение существующего когда без изменения его функциональности.
Впервые применять тестирование мы начали при доработке сайта «Финансы ТУТ» - раздела портала TUT.BY, на котором предоставлена актуальная информация о курсах валют, кредитах, вкладах и банках. Поэтому правильная работа с информацией и точные числовые расчеты здесь очень важны.
Конечно, начинать работу с тестами лучше всего с нового проекта, но этот ресурс подвергся серьезному рефакторингу, и применение метода разработки через тестирование оказалось кстати. При его первоначальной разработке в 2004 году не учитывалась возможность функционального или даже визуального изменения ресурса, поэтому коды некоторых скриптов напоминали «спагетти с соусом», где код на РНР был вперемежку с HTML-кодом, и выглядело это примерно так:
<form action=banks.html method=get>
<table width="100%" border="0" cellspacing="0" cellpadding="0" class=tbl>
<tr>
<th><b>Поиск банка по коду</b></th>
</tr>
<tr id=brd_none>
<td colspan="2">Поиск по коду: <input type="text" id=input size=4 name=codes> <input type="submit" value="Найти" class=btn></form><br></td>
</tr>
<tr>
<th><B>Поиск банка по названию и/или городу</b></th>
</tr>
<tr id=brd_none>
<td>
<form action=banks.html method=get><br>
<select name=city style="width:100%">
<option value="">-выберите город-</option>
<?
if (eregi($c,$city)) {echo("<option value='" . $c . "' SELECTED>" .$c. "</option>\n");}
else {
echo("<option value='" . $c . "'>" .$c. "</option>\n");
}
$sql = "SELECT DISTINCT `c_city` FROM `codes` ORDER BY `c_city`;";
$res = mysql_query($sql, $link);
while($ret = mysql_fetch_array($res, MYSQL_ASSOC)) {
$c=$ret[c_city];
if ($c !='Минск') {
if (eregi($c,$city)) {echo("<option value='" . $c . "' SELECTED>" .$c. "</option>\n");}
else {
echo("<option value='" . $c . "'>" .$c. "</option>\n");
}
}
//echo("<option value='" . $ret["c_city"] . "'>" . $ret["c_city"] . "</option>\n");
}
?>
</select>
Согласитесь, не очень удобно. Это страница поиска банков, над ней мы и будем работать. Естественно, не все страницы ресурса выполняли выборку данных из БД в формате MySQL, поэтому данные и сама инициализация подключения к БД были также прописаны в каждом файле.
Итак, для начала нам пришлось отказаться от процедурного программирования и перейти на объектное: ведь ООП и тестирование друг без друга невозможны.
Далее описаны несколько простых тестов, которые помогут нам проверить работоспособность скрипта.
Шаг 1. Подготовка.
Чтобы нам было проще тестировать, а в дальнейшем и подвергать наши скрипты рефакторингу, мы выделим часть PHP-кода – например, данные для подключения к MySQL вынесем в отдельный файл config.php. Для начала мы посмотрим, как работает наш поиск, для этого подготовим тестовую среду и напишем первый тест. Все наши файлы будут храниться в корневой директории Web-сервера, а Simple Test мы распакуем в папку /test/.
На этом шаге в корневой директории у нас будет всего три файла: config.php – скрипт, содержащий конфигурационные данные, runner.php –скрип, который будет прогонять все наши тесты, и скрипт, содержащий сами тесты - all_my_test.php.
Config.php
<?
define('SIMPLE_TEST_DIR','test/');
require_once(SIMPLE_TEST_DIR.'web_tester.php');
require_once(SIMPLE_TEST_DIR.'reporter.php');
require_once(SIMPLE_TEST_DIR.'unit_tester.php');
?>
Runner.php
<?
require_once('config.php');
class MyTest extends GroupTest {
function MyTest() {
$this->GroupTest('My test');
//Подключаем все наши тесты, которые будут выполняться
$this->addTestFile('all_my_test.php');
}
}
$test = new MyTest();
if(SimpleReporter::inCli()) {
exit($test->run(new TextReporter())?0:1);
}
$test->run(new HtmlReporter());
?>
Сейчас мы напишем тест, который проверит, как работает наш поиск:
<?
class TestMyProject extends WebTestCase {
function test_search() {
//имитируя работу браузера, заходим на нужную нам страницу
$this->get('https://finance.tut.by/banks.html');
}
function test_search_form() {
$this->_input_data('220');
$this->_test_result();
}
function _input_data($codes) {
//имитируем ввод данных и нажатие на кнопку
$this->setField('codes',$codes);
$this->clickSubmitByName('submit');
}
function _test_result() {
//ищем индекс банка, чтобы проверить, что поиск работает
$this->assertWantedPattern('/220101/s');
}
}
?>
При запуске runner.php мы убедимся, что все тесты сработали (об этом нам сообщит зеленая полоска), после чего можно приступать к написанию более сложных тестов. Кстати, запустить все наши тесты можно также и из командной строки.
Шаг 2. Усложняем наши тесты.
Для дальнейшей работы мы написали класс для реализации поиска. Вот несколько методов, которые мы потом будем тестировать:
<?
class Bank{
var $codes='';
var $bank_id='';
var $street_id='';
function Bank($codes,$bank_id,$street_id) {
$this->bank_id = $bank_id;
$this->codes = $codes;
$this->street_id = $street_id;
}
function _GetCodes() {
return $this->codes;
}
function _GetBankId() {
return $this->bank_id;
}
function _GetStreetId() {
return $this->street_id;
}
function _GetRequestArray() {
$query="SELECT * FROM tbl_banks WHERE codes='{$this->codes}' OR bank_is='{$this->bank_id}' OR street_id='{$this->street_id}'";
$result=mysql_query($query);
$row = mysql_fetch_array($result);
return $row;
}
}
?>
Тут есть не только конструктор, но и так называемые геттеры – методы, возвращающие необходимые данные.
Нам важно, чтобы все данные, входящие в наш класс, были нужного нам типа и вообще присутствовали. Напишем небольшой тест для этого. Файл all_my_test.php будет выглядеть следующим образом:
<?
require_once('bank.class.php');
class TestMyProject extends UnitTestCase {
function TestCodes() {
$bank = new Bank('1','2','3');
$this->assertTrue($bank->_GetCodes());
$this->assertNotA($bank->_GetCodes(),'int');
}
}
?>
Тут все очень просто: вначале проверяем на наличие нашей переменной codes
$this->assertTrue($bank->_GetCodes());
– а затем на соответствие переменной нужному нам типу.
$this->assertNotA($bank->_GetCodes(),'int');
Точно таким же образом можно проверить и на работоспособность функции, выполняющие SQL запрос:
<?
require_once('bank.class.php');
class TestMyProject extends UnitTestCase {
function TestCodes() {
$string = (int)0;
$bank = new Bank('1','1','1');
$this->assertTrue($bank->_GetCodes());
$this->assertNotA($bank->_GetCodes(),$string);
}
function TestRequest() {
$bank = new Bank('1','2','3');
$this->assertTrue($bank->_GetRequestArray());
}
}
?>
Кроме проверки на простейшее соответствие текущая версия Simple Test имеет следующие тестовые функции:
assertTrue($x) — выдается ошибка, если значение False;
assertFalse($x) — получаете ошибку тестирования, если переданное значение True;
assertNull($x) — проверка, существует ли переданное значение;
assertNotNull($x) — указанное в параметрах метода значение не должно существовать;
assertIsA($x, $t) — переменная $x не должна иметь тип $t;
assertNotA($x, $t) — проверка на соответствие типа переменной $x строке $t;
assertEqual($x, $y) — проверка на равенство $x и $y;
assertNotEqual($x, $y) — проверка на неравенство $x и $y;
assertWithinMargin($x, $y, $m) — проверка того, что разница $x и $y меньше $m;
assertOutsideMargin($x, $y, $m) — разница $x и $y больше $m;
assertIdentical($x, $y) — проверка полной идентичности (одновременно типа и значения переменных);
assertNotIdentical($x, $y) — проверка неидентичности значений;
assertReference($x, $y) — ошибка выдается, если $y не является ссылкой на переменную $x;
assertCopy($x, $y) — в случае если $y — ссылка на $x, выдается ошибка;
assertPattern($p, $x) — проверка строки $x по регулярному выражению $p;
assertNoPattern($p, $x) — несоответствие строки $x регулярному выражению $p;
assertNoErrors() — при выполнении кода не было ошибок;
assertError($x) — в коде должна быть ошибка.
Кстати, бывает так, что некоторые разработанные методы используют в своей работе другие методы, которых пока нет. Естественно, в этом случае тесты не срабатывают. На помощь в таком случае приходят Мок-объекты. Мок-объекты – это заглушки в коде, которые могут имитировать работу необходимых методов и даже целых классов.
Например, нам нужно имитировать работу класса, выполняющего подключение к базе данных. Это может выглядеть так:
<?
require_once('simpletest/unit_tester.php');
require_once('simpletest/mock_objects.php');
require_once('database_connection.php');
Mock::generate('DatabaseConnection');
class MyTestCase extends UnitTestCase {
function testSomething() {
$connection = &new MockDatabaseConnection();
}
}
?>
Таким же образом можно создать методы имитируемого класса и заставить их возвращать необходимые данные. Подробнее о Мок-объектах можно прочитать на сайте http://simpletest.sourceforge.net .
Приведенные выше тесты весьма и весьма просты, однако помогают на начальном этапе проверить работоспособность скрипта и избежать неприятностей в дальнейшем. Конечно, в своей работе мы используем тесты намного сложнее и длиннее описанных в этой статье, а их количество в процессе разработки достигает нескольких сотен.
В этой статье основное внимание было уделено модульному тестированию, или проверке кода программы на «живучесть». Но это лишь одна часть тестирования. Как я уже отмечал, есть еще и приемочное тестирование. Одним из наиболее распространенных пакетов для эмуляции браузера и проверки работоспособности сайта является Selenuim http://www.openqa.org/selenium.
Вся прелесть среды тестирования заключается в том, что оно представляет собой простое объектно-ориентированное JavaScript-приложение, которое не составляет труда настроить под свои нужды. Чтобы начать работать с Selenium, его достаточно распаковать в любую директорию на Web-сервере и запустить через браузер файл TestRunner.html. Примеры тестов, написанных для этого инструмента, есть в стандартной поставке. Более того, с помощью этих тестов Selenium может протестировать даже сам себя!
К сожалению, тестирование еще не так распространено среди РНР-разработчиков настолько, насколько хотелось бы. Причин много: плохое понимание ООП, нежелание работать быстрее и качественнее или просто незнание о существовании пакетов тестирования.
Для тех, кто всерьез задумывается о переходе на такой метод разработки своего ПО, я бы хотел дать несколько советов:
• Делайте компактные тесты.
Не забывайте о том, что тесты могут служить документацией к вашему коду, поэтому старайтесь делать их как можно более короткими и понятными. Не старайтесь в один тестовый метод «засунуть» проверку всего сразу. Также не забывайте о том, что тесты не должны зависеть от внешних ресурсов.
• Улучшайте Ваши тесты.
Рефакторинг – неотъемлемая часть метода разработки через тестирование. Причем нужно подвергать рефакторингу не только код ваших тестов, но также и тестируемый код.
Таким образом, при применении данных методик качество и скорость разработки увеличивается, а количество ошибок в коде уменьшается. Не это ли, в конце концов, требуется от хорошего программиста?
Сведения об авторе
Виктор Комягин – инженер-программист УП «Надежные программы». Занимается разработкой различных сервисов белорусского портала TUT.BY. С ним можно связаться по e-mail victor@tutby.com.