Разработка через тестирование

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('http://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.