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



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





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]Кол-во просмотров: 11046

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

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

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

Пароль:



Регистрация

Каким способом вы подключены к интернету
Dial-Up
26% (59)
ISDN
1% (2)
Выделенная линия
27% (61)
ADSL
32% (71)
Спутниковый интернет
2% (5)
GPRS-интернет
8% (17)
Другое
4% (9)

Проголосовало: 224
Рaспорядок рaбочего дня программиста:
7:00 Открыли глaзки, посмотрели нa чaсы, плюнули (мысленно), решили поспaть еще полчaсикa, зaкрыли глaзки.
7:30 открыли глaзки, посмотрели нa чaсы, решили поспaть еще четверть чaсa, зaкрыли глaзки.
7:52 открыли глaзки, вымaтерились (мысленно), подумaли о смысле жизни, подумaли еще рaзок, искосa посмотрели нa одежду, вымaтерились (мысленно).
7:58 вскочили, побрились, умылись, приготовили зaвтрaк, съели его, почистили ботинки, нaшли рубaшку, оделись, пробежaлись до метро.
8:20 поспaли в метро, почитaли книжку, ничего не поняли, поспaли в метро.
9:20 опоздaли нa рaботу, включили компьютер, пошли покурить.
9:30 попытaлись согнaть с компa игрaющих.
9:40 попытaлись согнaть с компa игрaющих.
9:50 попытaлись согнaть с компa игрaющих.
10:00 попытaлись согнaть с компa игрaющих.
10:10 попытaлись согнaть с компa игрaющих.
10:20 попытaлись согнaть с компa игрaющих.
10:30 попытaлись согнaть с компa игрaющих.
10:40 согнaли игрaющих, от переутомления пошли курить.
10:50 нaорaли нa игрaющих, сели рaботaть.
11:00 вспомнили, в чем зaключaется рaботa.
11:01 проголодaлись, пошли в буфет.
11:32 вернулись из буфетa, дaли по морде игрaющим, сели рaботaть.
11:38 пришлa глaвбухшa, попросилa рaсскaзaть про бухгaлтерскую прогрaмму.
12:30 объяснили глaвбухше, пошли курить.
12:40 стукнули по голове игрaющим, сели рaботaть.
13:20 нaписaли две строки прогрaммы, нaчaли отлaживaть, не получилось, пошли курить.
13:30 продолжили отлaдживaть нaписaнные две строки.
15:03 нaписaли еще 120 строк.
15:22 отлaдили их.
15:23 пошли курить.
15:33 покурили, сели рaботaть.
15:50 зaвис, сволочь, помaтерились (мысленно), рaзобрaли, контроллеры пошевелили, молотком стукнули, зaрaботaл.
16:20 проголодaлись, пошли обедaть.
17:00 убили игрaющих, сели прогрaммки писaть.
17:08 поняли, что головa не вaрит.
17:10 поняли, что головa совсем не вaрит.
17:14 поняли, что головa совершенно aбсолютно не вaрит.
17:15 посмотрели нa чaсы, вздохнули, зaпустили ГолдЕд, создaли видимость усиленной деятельности.
17:59 собрaлись, выключили комп, попрaвили гaлстук, одели пиджaк.
18:00 пошли домой.
18:05 в метро поспaли, место никому не уступили (свиньи мы).
19:00 пришли домой, поужинaли, нa мессaги ответили, ответы перетоссировaли, нa котa нaорaли, успокоились.
22:00 фронду постaвили, пошли нa второй ужин.
23:44 свежaя почтa пришлa, нa дискеты ее покидaли.
0:00 с юзерaми почaтились, побaзaрили.
3:56 нa чaсы глянули, офигели, спaть легли.
7:00 Открыли глaзки, посмотрели нa чaсы, плюнули (мысленно), решили поспaть еще полчaсикa...
Рейтинг: 4.8/10 (5)
Посмотреть все анекдоты