Пока не совсем понятно, что именно из себя будет представлять 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'));
}
}
?>