Tdd lekcja numer 2

Programowanie jest trudne. Wymaga ciągłej perfekcji przez miesiące i lata pracy. W najlepszym przypadku pomyłki prowadzą do kodu, który się nie kompiluje. W najgorszym – do błędów czekających w ukryciu, by ujawnić się w momencie, w którym spowodują największe szkody.
Ludzie rzadko pracują perfekcyjnie. Nie dziwi więc, że oprogramowanie zwykle ma błędy.
Czyś nie byłoby przydatne narzędzie, które informowałoby o pomyłkach programistycznych bezpośrednio po ich popełnieniu; narzędzie tak skuteczne, że debugowanie stałoby się niemal niepotrzebne?
James Shore, Shane Warder, Agile Development. Filozofia programowania zwinnego.

Tym narzędziem miałoby być TDD, więc nie ma na co czekać – drugie spotkanie wytwarzaniem sterowanym testami.
Zacznę od momentu, gdzie skończyłem poprzednio: http://blog.grzegorzpawlik.com/2009/05/zaczynamy-test-driven-development/

Ciekawe wrażenie: mimo tego, że nie ma żadnego interfejsu mam poczucie, że program działa jak należy ;)

Mam pewną zagwozdkę. Otóż powinienem zacząć od zmiany testu. Pomyślałem o tym, żeby metoda getLastTopics zwracała zadaną w parametrze ilość ostatnich tematów. Jednak dodanie parametru przy wywołaniu tej metody nie prowadzi do błędu. To specyfika php, więc na razie wyłamię się z cyklu i najpierw zmienię sygnaturę metody. Trudno.

//models/topic.php
function getLastTopics($ammount) {
return $this->find('all');
}

Oczywiście metoda nie przeszła testu, poprawię więc test (sic!)

//tests/cases/models/topic.test.php
function testGetLastTopics() {
$topics = $this->Topic->getLastTopics(5);
$this->assertTrue(is_array($topics));
$expected = array( array( 'Topic' => array('id'=>1, 'title'=>'Pierwszy post') ) );
$this->assertEqual($topics, $expected);
}

Test przeszedł. Mogę wrócić do zalecanej kolejności. Znów popsuję test, bo chcę dostać 3 ostatnie posty:

//topic.test.php
function testGetLastTopics() {
$topics = $this->Topic->getLastTopics(5);
$this->assertTrue(is_array($topics));
$expected = array( array( 'Topic' => array('id'=>1, 'title'=>'Pierwszy post') ),
array( 'Topic' => array('id'=>2, 'title'=>'Jakiś post')),
array( 'Topic' => array('id'=>3, 'title'=>'Trzeci post')),
);
$this->assertEqual($topics, $expected);
}

Dostałem błąd, jednak aby go poprawić – muszę poprawić fixtures, gdyż danych testowych jest za mało…

//tests/fixtures/topic_fixture.php
var $records = array(
array('id'=> 1, 'title' => 'Pierwszy post'),
array('id'=> 2, 'title' => 'Jakiś post'),
array('id'=> 3, 'title' => 'Trzeci post')
);

Test ok, jednak wolę, żeby te tematy były w kolejności od najnowszego do najstarszego:

//topic.test.php
$expected = array(
array( 'Topic' => array('id'=>3, 'title'=>'Trzeci post')),
array( 'Topic' => array('id'=>2, 'title'=>'Jakiś post')),
array( 'Topic' => array('id'=>1, 'title'=>'Pierwszy post') ),
);

Test nie przechodzi, więc poprawiam model:

//topic.php
class Topic extends AppModel {
var $name = "Topic";
function getLastTopics($ammount) {
return $this->find('all', array('order'=> "Topic.id DESC"));
}
}

Wszystko ok, więc sprawdzam, czy dostanę dwa najnowsze elementy:

//topic.test.php
function testGetLastTopics() {
$topics = $this->Topic->getLastTopics(3);
$this->assertTrue(is_array($topics));
$expected = array(
array( 'Topic' => array('id'=>3, 'title'=>'Trzeci post')),
array( 'Topic' => array('id'=>2, 'title'=>'Jakiś post')),
array( 'Topic' => array('id'=>1, 'title'=>'Pierwszy post') ),
);
$this->assertEqual($topics, $expected);

$topics = $this->Topic->getLastTopics(2);
$expected = array(
array( 'Topic' => array('id'=>3, 'title'=>'Trzeci post')),
array( 'Topic' => array('id'=>2, 'title'=>'Jakiś post')),
);
$this->assertEqual($topics, $expected);
}

Test nie przechodzi, choć wydawało mi się, że powinien przejść… no tak, dodałem parametr ammount, ale dalej zwracam wszystkie elementy, szybka poprawka:

class Topic extends AppModel {
var $name = "Topic";
function getLastTopics($ammount) {
return $this->find('all', array('order'=> "Topic.id DESC", 'limit'=>$ammount));
}
}

I znów jest ok.

Chociaż rozpisałem się tu na dwa wpisy w blogu, to samo pisanie zajęło mi może 15 minut. Działającego kodu wprawdzie nie ma sporo, ale bez większego bólu jest pokryty w 100% testami i mam też dane startowe.

Czy możemy przetestować zapisywanie danych do bazy? Spróbujmy, zaczynając jak zwykle od testu. Jeśli wszystko działa jak należy to po zapisaniu danego element mogę go dostać wywołując metodę getLastTopics z parametrem 1:

function testSave() {
$new_topic = array('title'=>'Post testowy');
$this->Topic->save($new_topic);
$newest = $this->Topic->getLastTopics(1);
$this->assertEqual($new_topic['title'], $newest[0]['Topic']['title']);
}

Zauważ, że nie porównuję tu całej tablicy, a tylko wartość `title`. Zakładam, że nie wiem jakiego id się spodziewać, a lekką przesadą byłoby wywołanie getLastInsertId() i sprawdzenie czy się zgadza sama ze sobą.
Nie jest to może najpotrzebniejszy test, bo sprawdziliśmy metodę, którą model Title dziedziczy z frameworka, która z pewnością jest przetestowana. Jednak przynajmniej teraz wiem, że da się.

Co dalej?
Może warto, żeby tematy miały jakieś posty?
Test:

//post.test.php
App::import('model', 'Post');
class PostTestCase extends CakeTestCase {
var $name="post";
/**
* @var Post
*/
var $Post;
function start() {
parent::start();
$this->Post = ClassRegistry::init('Post');
}
}

Test nie przechodzi z powodu braku bazy. Ale spróbujmy najpierw dodać model.

//models/post.php
class Post extends AppModel {
var $name = "Post";
}

Nadal marudzi o brak bazy, więc trzeba będzie stworzyć fixture:

class PostFixture extends CakeTestFixture {
var $name = 'Post';
var $fields = array(
'id' => array('type'=> 'integer', 'key'=>'primary'),
'topic_id' => array('type'=> 'integer', 'null'=> false)
);
}

Spróbujmy odczytać dany post:

//post.test.php
function testFind() {
$post = $this->Post->findById(1);
$expected = array('Post' => array('id'=> 1, 'topic_id'=>1));
$this->assertEqual($post, $expected);
}

Dostaniemy błąd spowodowany brakiem danych testowych, naprawimy to dodając w post_pixture.php:

var $records = array( array('id'=> 1, 'topic_id'=>1),
);

Jednak Post zawsze powinien należeć do tematu:

function testFind() {
$post = $this->Post->findById(1);
$expected = array(
'Post' => array('id'=> 1, 'topic_id'=>1),
'Topic' => array('id'=>1, 'title'=>'Pierwszy post'),
);
$this->assertEqual($post, $expected);
}

Załadujmy jeszcze potrzebny topic_fixture:

var $fixtures = array('app.post', 'app.topic');

A teraz potrzebną relację, która sprawi, że z Postem dostaniemy topic:

class Post extends AppModel {
var $name = "Post";
var $belongsTo = array('Topic');
}

Jesteśmy na zielonym…
post_pass
… więc możemy skończyć. Jak na razie nie ma tu żadnych wypasów. Pocieszam się jednak, że to jak ze standardowym “Hellow world”. Nie zachwyca, ale nas zachwyca ;)

Share Button

One thought on “Tdd lekcja numer 2

  1. Pingback: Zaczynamy Test Driven Development « webbricks

Leave a Reply

Your email address will not be published. Required fields are marked *