» Главная
eXcode.ru » Статьи » PHP » Трюки
» Новости
» Опросы
» Файлы
» Журнал



Пользователей: 0
Гостей: 11





TDD: Дальнейший рефакторинг приложения




Дальнейший рефакторинг приложения

Создание FeedbackActiveRecord и введение модульных тестов в проект

Если приглядеться к index.php, то можно заметить, что хотя нам и получилось сделать его более читабельным, однако в коде есть определенные «нехорошие» связи с базой данных, явно требующие инкапсуляции. Неплохо было бы всю функциональность по работе с БД, выделить в отдельную сущность. Прежде чем, мы начнем это делать, добавим модульные тесты в приложение.

Модульные тесты

SimpleTest предоставляет средства для модульного тестирования при помощи класса UnitTestCase. Как и WebTestCase, UnitTestCase позволяет иметь набор тестовых методов/прецедентов, начинающихся с ключевого слова test. Как видно из названия, UnitTestCase не предоставляет средств для фукционального web тестирования, его основной целью является предоставление инструментария для организации утверждений по ожидаемой работе тестируемой сущности.

Для этого измненим /tests/runtests.php:

<?php
[...]
class AllTests extends GroupTest {
    function AllTests() {
        $this->GroupTest('All tests for feedback project');
        //$this->addTestFile('acceptance_tests.php');
        $this->addTestFile('unit_tests.php');
    }
} 
?>

На время закоментируем выполнение функциональных тестов, чтобы сделать выполнение модульных тестов мгновенным. Создадим также файл tests/unit_tests.php:

<?php
class TestOfFeedbackActiveRecord extends UnitTestCase {
 
    function setUp() {
        DBC :: execute('DELETE FROM feedback');
    } 
}
?>

Первое что пришло в голову - создание самой простой фикстуры, а именно, очистка таблицы feedback перед каждым тестовым прецедентом. Почему именно TestOfFeedbackActiveRecord? Потому что в данной ситуации вполне уместно воспользоваться паттерном ActiveRecord для инкапсуляции отображения сообщения в БД.

Реализация FeedbackActiveRecord

Пока не совсем понятно, что именно из себя будет представлять FeedbackActiveRecord, можно сделать очень простые тесты на сеттеры/геттеры. Во время написания тестов зачастую бывает так, что совершенно неясно, что именно требуется сделать, в такой ситуации написание тестов на, казалось бы, простейшую функциональность на самом деле развивает ход мысли разработчика на подсознательном уровне.

<?php
require_once(dirname(__FILE__) . '/../feedback.inc.php');
 
class TestOfFeedbackActiveRecord extends UnitTestCase {
[...]
    function testOfSettersGetters() {
        $feedback = new Feedback($name1 = 'Bobby',
                                 $email1 = 'email@dot.com',
                                 $message1 = "This a message",
                                 $time1 = time(),
                                 $id = 10);
 
        $this->_compareWithFeedback($feedback , $id, $name1, $email1, $message1, $time1);
 
        $feedback->setName($name2 = 'Bobby2');
        $feedback->setEmail($email2 = 'email2@dot.com');
        $feedback->setMessage($message2 = "This a message2");
        $feedback->setTime($time2 = time() + 10);
 
        $this->_compareWithFeedback($feedback , $id, $name2, $email2, $message2, $time2);
    }
 
    function _compareWithFeedback($feedback, $id, $name, $email, $message, $time)
    {
        $this->assertEqual($feedback->getId(), $id);
        $this->assertEqual($feedback->getName(), $name);
        $this->assertEqual($feedback->getEmail(), $email);
        $this->assertEqual($feedback->getMessage(), $message);
        $this->assertEqual($feedback->getTime(), $time);
    }
}
?>

Локально мы также применили небольшой рефакторинг, выделив метод compareWithFeedback, тем самым сделав тело теста более читабельным.

После попытки выполнить тест мы, естественно, получили parse error, т.к класса Feedback еще даже и не существует. Самое время сделать его простейшую реализацию в файле feedback.inc.php:

<?php
class Feedback {
    var $id;
    var $name;
    var $email;
    var $message;
    var $time;
 
    function Feedback($name, $email, $message, $time, $id=NULL) {
        $this->name = $name;
        $this->email = $email;
        $this->message = $message;
        $this->time = $time;
        $this->id = $id;
    }
 
    function getId() {
        return $this->id;
    }
 
    function getName() {
        return $this->name;
    }
 
    function setName($name) {
        $this->name = $name;
    }
 
    function getEmail() {
        return $this->email;
    }
 
    function setEmail($email) {
        $this->email = $email;
    }
 
    function getMessage() {
        return $this->message;
    }
 
    function setMessage($message) {
        $this->message = $message;
    }
 
    function getTime() {
        return $this->time;
    }
 
    function setTime($time) {
        $this->time = $time;
    }
 }
?>

Убедившись в положительном результате тестирования, перейдем к реализации сохранения объекта Feedback в БД. Очень удобно иметь в интерфейсе данного объекта единый метод save, который бы в зависимости от внутреннего состояния Feedback, либо его вставлял, либо обновлял в БД. Для начала протестируем ситуацию, когда объект у нас является новым:

<?php
class TestOfFeedbackActiveRecord extends UnitTestCase {
[...]
    function testOfSaveInsertNew() {
        $feedback = new Feedback('Bobby',
                                 'email@dot.com',
                                 'This a message',
                                 time());
 
        $this->assertNull($feedback->getId());
        $feedback->save();
        $id = $feedback->getId();
 
        $rs = DBC :: NewRecordSet('SELECT * FROM feedback');
        $this->assertEqual($rs->getTotalRowCount(), 1);
 
        $rs->reset();
        $rs->next();
        $this->_compareWithRS($rs, $feedback);
    }
 
    function _compareWithRS($rs, $feedback)
    {
        $this->assertEqual($rs->get('id'), $feedback->getId());
        $this->assertEqual($rs->get('name'), $feedback->getName());
        $this->assertEqual($rs->get('email'), $feedback->getEmail());
        $this->assertEqual($rs->get('message'), $feedback->getMessage());
        $this->assertEqual($rs->get('time'), $feedback->getTime());
    }
}
?>

Опять же в целях читабельности мы ввели метод _compareWithRS, проверяющий некоторый объект Feedback с непосредственной выборкой из базы данных. Убедившись в «неисполнимости» данного тестового случая, приступаем к реализации метода save.

<?php
class Feedback {
[...]
    function save() {
        if(is_null($this->id)) {
            $this->id = $this->_insert();
        } else {
            $this->_update();
        }
    }
 
    function _insert() {
        $record =& DBC::NewRecord($this->_makeDataSpace());
        return $record->insertId('feedback', array('name', 'email', 'message', 'time'), 'id');
    }
 
    function _update(){}
 
    function & _makeDataSpace() {
        $dataspace = new DataSpace();
        $dataspace->import(array('name' => $this->name,
                                 'email' => $this->email,
                                 'message' => $this->message,
                                 'time' => $this->time));
        return $dataspace;
    }
}
?>

Как видно из кода, мы исходим из простого предположения о том, что если у объекта id === NULL, значит он является новым, а, следовательно, при вызове save его надо поместить в БД. WACT DBAL работает с данными, которые располагают в контейнере класса DataSpace, поэтому нам также пришлось ввести внутренний метод _makeDataSpace(). Стоит заметить, что на месте метода _update пока располагается пустая заглушка, позволяющая однако тестам выполняться. Одной из центральных идей TDD является как можно более быстрое срабатывание тестов даже при самой слабой реализации. В дальнейшем слабая реализация будет постепенно отрефакторена на последующих итерациях.

Попробуем выразить ожидаемую работу метода save уже для существуещего объекта при помощи тестов:

<?php
class TestOfFeedbackActiveRecord extends UnitTestCase {
[...]
    function testOfSaveUpdate() {
        $feedback1 = new Feedback('Bobby1',
                                  'email1@dot.com',
                                  'This a message1',
                                  time());
        $feedback1->save();
 
        $feedback2 = new Feedback('Bobby2',
                                  'email2@dot.com',
                                  'This a message2',
                                  time() - 10);
        $feedback2->save();
 
        $feedback2->setName('Bobby3');
 
        $feedback2->save();
 
        $rs = DBC :: NewRecordSet('SELECT * FROM feedback');
        $this->assertEqual($rs->getTotalRowCount(), 2);
 
        $rs->reset();
        $rs->next();
        $this->_compareWithRS($rs, $feedback1);
        $rs->next();
        $this->_compareWithRS($rs, $feedback2);
    }
}
?>

Заметьте, мы использовали одновременно 2 объекта класса Feedback, сделано это для того, чтобы удостовериться, что второй объект никаким образом не был затронут после вызова метода save. Дело в том, что фикстура полностью удаляет содержимое таблицы feedback, и если бы мы проводили тесты, работая только с одним объектом, тесты бы полностью не покрывали ожидаемой функциональности.

Тест не сработал, пора браться за реализацию метода _update:

<?php
class Feedback {
[...]
    function _update() {
        $record =& DBC::NewRecord($this->_makeDataSpace());
        $record->update('feedback', array('name', 'email', 'message', 'time'),
            "id=" . DBC::makeLiteral($this->id));
    }
}
?>

Итерфейс Feedback получается чистым, но для полноты в нем не хватает метода delete.

<?php
class TestOfFeedbackActiveRecord extends UnitTestCase {
[...]
    function testOfDelete() {
        $feedback1 = new Feedback('Bobby1',
                                  'email1@dot.com',
                                  'This a message1',
                                  time());
        $feedback1->save();
 
        $feedback2 = new Feedback('Bobby2',
                                  'email2@dot.com',
                                  'This a message2',
                                  time() + 10);
        $feedback2->save();
 
        $feedback2->delete();
 
        $rs1 = DBC :: NewRecordSet('SELECT * FROM feedback');
        $this->assertEqual($rs1->getTotalRowCount(), 1);
 
        $rs1->reset();
        $rs1->next();
        $this->_compareWithRS($rs1, $feedback1);
 
        $feedback2->save();
 
        $rs2 = DBC :: NewRecordSet('SELECT * FROM feedback');
        $this->assertEqual($rs2->getTotalRowCount(), 2);
 
        $rs2->reset();
        $rs2->next();
        $this->_compareWithRS($rs2, $feedback1);
        $rs2->next();
        $this->_compareWithRS($rs2, $feedback2);
    }
}
?>

Как и в предыдущем примере, мы проверяем работу метода delete(), при работе с несколькими объектами Feedback, тем самым проверяя его на безопасность. Тест также проверяет тот факт, что после того, как у объекта вызвали метод delete(), а затем метод save(), объект будет вновь помещен в БД.

Привычным образом получив «красную полосу», приступаем к реализации:

<?php
class Feedback {
[...]
    function delete() {
        if(is_null($this->id)) {
            return;
        }
        DBC::execute("DELETE FROM feedback WHERE id=". DBC::makeLiteral($this->id));
        $this->id = null;
    }
}
?>

Остается только вопрос, куда поместить методы поиска Feedback записей. Самое простое решение - пока поместить их непосредственно Feedback, сделав статическими. Начнем с теста, который бы нам возвращал список всех записей из таблицы, используя ограничивающий пейджер.

<?php
class TestingPagerStub{
    function getStartingItem(){return 1;}
    function getItemsPerPage(){return 1;}
    function setPagedDataSet($dataset){}
}
 
class TestOfFeedbackActiveRecord extends UnitTestCase {
[...]
    function testOfGetList() {
        $feedback1 = new Feedback('Bobby1',
                                  'email1@dot.com',
                                  'This a message1',
                                  time());
        $feedback1->save();
        $id1 = $feedback1->getId();
 
        $feedback2 = new Feedback('Bobby2',
                                  'email2@dot.com',
                                  'This a message2',
                                  time() - 10);
        $feedback2->save();
        $id2 = $feedback2->getId();
 
        $pager = new TestingPagerStub();
        $rs =& Feedback :: getList($pager);
 
        $rs->reset();
        $rs->next();
        $this->_compareWithRS($rs, $feedback2);
 
        $this->assertEqual($rs->getRowCount(), 1);
        $this->assertEqual($rs->getTotalRowCount(), 2);
 
        $pager->tally();
    }
}
?>

Можно попробовать реализовать метод getList, который, благодаря WACT оказался простым до безобразия:

<?php
class Feedback{
[...]
    function &getList(&$pager) {
        return DBC::NewPagedRecordSet('SELECT * FROM feedback ORDER BY time DESC', $pager);
    }
}
?>

Еще раз изучив интерфейс Feedback, можно сказать, что логично также иметь статический фабричный метод load($rs), который бы нам позволял конструировать объекты Feedback на основе непосредственной выборки из БД.

<?php
class TestOfFeedbackActiveRecord extends UnitTestCase {
[...]
    function testOfLoad() {
        $feedback = new Feedback('Bobby1',
                                 'email1@dot.com',
                                 'This a message1',
                                 time());
        $feedback->save();
 
        $rs = DBC :: NewRecordSet('SELECT * FROM feedback');
        $rs->reset();
        $rs->next();
 
        $this->assertEqual($feedback,
                           Feedback :: load($rs));
    }
}
?>

Ничуть не огорчившись из-за невыполняющегося теста, приступим к реализации:

<?php
class Feedback{
[...]
    function &load(&$rs) {
        return new Feedback($rs->get('name'),
                            $rs->get('email'),
                            $rs->get('message'),
                            $rs->get('time'),
                            $rs->get('id'));
    }
}
?>

Финальные штрихи

А вот теперь самое интересное, давайте попробуем использовать Feedback в нашем приложении. Для этого мы опять изменим index.php:

<?php
ob_start();
 
require_once(dirname(__FILE__) . '/external/wact/framework/common.inc.php');
require_once(dirname(__FILE__) . '/feedback.inc.php');
require_once(WACT_ROOT . '/template/template.inc.php');
 
if(isset($_POST['submit'])) {
    $feedback = new Feedback($_POST['name'], $_POST['email'], $_POST['message'], time());
    $feedback->save();
}
 
$page = new Template('/feedback.html');
$pager =& $page->getChild('pager');
 
$feedback =& $page->findChild('feedback');
$feedback->registerDataSet(Feedback :: getList($pager));
 
$page->display();
 
ob_end_flush();
?>

Также раскоментируем строку, включающую функциональные тесты в файле tests/runtests.php:

<?php
[...]
class AllTests extends GroupTest {
    function AllTests() {
        $this->GroupTest('All tests for feedback project');
        $this->addTestFile('acceptance_tests.php');
        $this->addTestFile('unit_tests.php');
    }
} 
[...]
?>

Вуаля! Наше приложение было полностью отрефакторено и переведено на рельсы TDD!

Далее - Шаг четвертый - расширяем функциональность приложения и тестируем отправку почты.

К началу статьи





Добавил: Дата публикации: 2008-03-05 09:09:01
Рейтинг статьи:2.00 [Голосов 1]Кол-во просмотров: 11379

Комментарии читателей

Всего комментариев: 1

2009-08-14 02:31:33
fonkil
Предлагаю рассылку рекламы и сообщений на форумы 12$ на 30000 форумов http://reklamada2009.narod.ru
e-mail reklam2009@narod.ru
Ваше имя: *
Текст записи: *
Имя:

Пароль:



Регистрация

Какой марки ваш мобильник?
Nokia
40% (146)
Samsung
8% (29)
Siemens
16% (59)
Motorola
13% (49)
Sony Ericsson
13% (49)
LG
1% (4)
Pantech
0% (0)
Alcatel
2% (6)
Другой
3% (10)
Нет у меня мобилы
4% (15)

Проголосовало: 367
- Как хакер взламывает банкомат?
1) Берёт с собой ноутбук и молоток
2) Подходит к банкомату
3) Разбивает банкомат молотком
4) Забирает деньги и уходит
- А зачем ему ноутбук?
- Ну а какой-же хакер без ноутбука!
Рейтинг: 0/10 (0)
Посмотреть все анекдоты