TDD не понравилось
Разработка через тестирование — весьма странное занятие.
Я попробовал на новом проекте и пока нахожусь в смятении. Наверняка дело в мышлении, которое осталось с прошлого опыта: сначала реализация, потом функциональное тестирование приложения. Покрытие кода тестами делалось обычно постфактум, и обычно тесты были не тупыми проверками работоспособности функций, а проверка связанного функционала, поведения то есть. Behavioral.
Мне не нравится все что я тут понаписал потому, что, несмотря на то, что код покрыт тестами, абстракция сосёт!
Но тем не менее. Давайте-ка посмотрим на то, что я тут понаписал.
Требовалось создать демона, который проверяет базу данных на наличие записи в БД с марекером created и делает полезную работу.
Итак, codeflow демона:
- найти записи с статусом created, взять данные
- изменить статус на processing
- сделать по данным из п.1 много полезной работы
- сохранить результаты работы в другой таблице
- изменить статус на done
Начал я создавать функциональность демона именно в той последовательности, в которой операции и должны проводиться. Как бы потому, что это последовательность действий, а не просто набор действий.
По всем правилам TDD я создал тест:
//Найти записи со статусом created
public function testGetCreatedOrders()
{
$daemon = new Daemon();
$result = $daemon->getCreatedOrders();
$this->assertTrue(is_array($result);
var_dump($result);
}
Потом был написан очень простой код в классе Daemon:
class Daemon
{
public function getCreatedOrders()
{
return Application_Model_Order::getOrderByOurStatus("created");
}
}
И вот тут первый вопрос у меня.
Нужен ли вообще тест на этот метод, при том, что тест на этот метод уже реализован для класса Application_Model_Order?
Конечно же, можно задать вопрос, нужен ли вообще этот однострочный метод в классе, но например, нужен. Нужен хотя бы по тому, что я только начал, и это полет мысли. Возможно, я потом передумаю, но вот здесь и сейчас мне нужна функциональность, и я просто хочу, чтобы она была такой.
Окей, идем дальше, ко второму пункту.
public function testMarkOrderProcessing()
{
$daemon = new Daemon();
$result = $daemon->markOrderProcessing(1);
$this->assertTrue(1==$result);
var_dump($result);
}
</pre>
Ололо, что нам нужно сделать:
<pre class="brush:php">
public function markOrderProcessing($orderId)
{
return Application_Model_Order::markOrder($orderId, 'processing');
}
Как удобно :)
Идем к третьему пункту.
public function testParseData()
{
$mock = "{"some":"data", "in":"json format"}";
$daemon = new Daemon();
$result = $daemon->necessaryWork($mock);
$this->assertTrue(isset($result['blabla']);
var_dump($result);
}
Отлично, в моем приложении уже появился какой-то интересный код в necessaryWork.
public function necessaryWork($orderId, $rawData)
{
$order = Zend_Json_Decoder::decode($rawData);
$someData = $this->_generateSomeData($order);
$elseData = $this->_doSomethingElse($order);
return array($someData, $elseData);
}
Идем к четвртому пункту.
public function testSaveInfo()
{
$mock = array('results'=>'of the work', 'of'=>'necessaryWork method');
$daemon = new Daemon();
$result = $daemon->saveData($mock);
$this->assertTrue(1==$result);
var_dump($result);
}
Условный код в классе:
public function saveData($data)
{
$obj = new Application_Model_Obj();
$obj->name = $data['name'];
$obj->city = $data['city'];
$obj->save();
}
Пятый пункт выполнился совершенно так же, как пункт 2.
Итак, у меня появилась полная функциональность моего демона реализованная через тесты. Осталось сделать один главный метод на демоне, который и будет делать всю работу. Тест:
public function testMain()
{
$daemon = new Daemon();
$result = $daemon->main;
$this->assertTrue(1==$result);
var_dump($result);
}
Рабочий метод в классе демона:
public function main()
{
//найти created
$order = $this->getCreatedOrders();
if (!isset($order[0])) {
return false;
} else {
$order = $order[0];
}
//пометить как processing
$this->markOrderProcessing($order['id']);
//сделать полезную работу
$necessaryData = $this->necessaryWork($order['id'], $order['necessarydata']);
//сохранить в бд
$this->saveData($necessaryData);
//пометить как done
$this->markOrderDone($order['id']);
echo 'done for id = ' . $order['id'] . PHP_EOL;
}
Вот и готов мой демон. Весь тестами покрыт, код читабельный, поддерживаемый.
Но что здесь не так?! Все методы public. Жутко бесит, хочется взять и убрать.
Если бы я не использовал TDD, пабликом у меня был бы только necessaryWork, т. к. только он является реально новым кодом, на который следует создавать тест.
Вот теперь как поступить с этим всем? Оставить все методы пабликами и пусть идет кровь из глаз, или все методы скрыть и оставить наружу только work(), который в цикле постоянно запускает private _main()?
Так-то становится понятным теперь, почему ребятки питонисты из соседней конторы шлют лесом всю инкапсуляцию в своих исходниках. Я надеюсь, что это вот вздор так останется ерундой.
P.S. разумеется, всё сокрыл и ненужные тесты потер. Нечего мне тут.
По теме:
Introduction to Test Driven Development
Простое написание тестов — это не TDD!
Ошибки начинающих TDD-практиков.
Думаю, бдд будет более уместно, т. к. при бдд нет необходимости покрывать тестами методы, относящиеся к протектед или прайвит. Бдд удобно также тем (личный опыт), что призывает разрабатывать только то, что нужно, без лишнего кода. В данном случае, наверное, следует проверить сценарии:
Бдд, лично, склонен считать направляющей колеей проектирования кода. Тдд, в чьи задачи не входит тестирование логики, увы, не способно гарантировать с высокой долей вероятности работоспособность функционала.