Archive for category TDD in cakePHP
Kocham UnitTesty*
Posted by Greg in Agile, CakePHP, TDD in cakePHP on July 29th, 2010

Dziś stanąłem przed zadaniem poprawienia komponentu, który automatyzuje nam kwestię wyszukiwania elementów w listingach.
Jego działanie można opisać z grubsza tak:
Na przykład jeśli chcę, żeby moja lista kosztów (Cost) mogła być wyszukiwana po nazwach – dodaję formularz z Cost.name i w akcji kontrolera wywołuję jedynie
$conditions = $this->Search->getConditions($this->params);
$this->paginate("conditions"=>$conditions);
$this->set('costs', $this->paginate('Cost'));
Największy bajer polegał na tym, że jeżeli moje koszty były powiązane z samochodami (Car) to chcą wyszukiwać po nazwie samochodu wystarczyło, że dodałem do formularza pole Car.name. Mój komponent potrafił zorientować się, że to jest powiązany model, wyszukać elementy spełniające warunek, a następnie zwrócić w conditions coś takiego:
Array
(
[and] => Array
(
[Cost.id] => Array
(
[0] => 1
[1] => 2
[2] => 5
[3] => 9
[4] => 10
[5] => 13
[6] => 17
[7] => 18
)
)
)
Gdzie cake ładnie sobie to przerabiał na zapytanie (w przybliżeniu)
Select * from costs as Cost where Cost.id IN (1, 2, 5, 9, 10, 13, 17, 18)
Byłem bardzo zadowolony z komponentu. Służył nam dzielnie przez przynajmniej dwie ostatnie iteracje. Każdą wyszukiwarkę/filtr dodawaliśmy w 5 minut na żądanie klienta (wewnętrznego
).
Mało tego, dość dużym wyzwaniem było sporządzenie testów dla tego komponentu (kilka MockObjects nawet się tam pojawiło) – pewnie dlatego tak sumiennie je wykonałem – 91% (z hakiem!) pokrycia kodu przez testy.
Przyszedł jednak dzień, gdy podczas prezentacji iteracji szef (klient wewnętrzny) powiedział (cytat nie dosłowny):
“Chcę jeszcze filtrować koszty wg numerów faktur (Invoice.number) z którymi te koszty są powiązane”
Problem w tym, że koszty są powiązane z fakturami poprzez elementy faktur (InvoiceElement), a konkretniej
Cost habtm InvoiceElement belongsTo Invoice
Zatem przyszło mi poprawić komponent, przy czym szczęśliwie zdążyłem zapomnieć wszystkich niuansów, foreachów i innych takich, które harcowały wewnątrz search_component.php. Myślę sobie – ok, tak jak poprzednio – dużo testów. Ale nie wiedziałem jak się do nich zabrać. Wiedziałem jedynie, że nazwa pola dla nowego przypadku powinna zawierać ścieżkę powiązań (np. InvoiceElement.Invoice.number).
Mając jednak w głowie dość przydatną radę na temat unit testów, która brzmi:
Najlepsze są testy napisane przed kodem. Jednocześnie napisanie najpierw kodu, a potem testów jest o niebo lepsze od braku jakichkolwiek testów.
Dlatego postanowiłem od razu przejść do próby implementacji nowych elementów dbając jedynie o kondycję dotychczasowych testów.
To co się wydarzyło można nazwać chyba “uprzężą testową”. Mogłem odważnie śmigać po dotychczasowo napisanym kodzie, bo w tym samym czasie zielone pole w rezultatach testów mówiło mi “śmiało dalej! niczego do tej pory nie popsułeś!”. Wspaniałe uczucie.
Wprawdzie po skończeniu pokrycie kodu spadło do 80% (nota bene i tak świetny wynik, 70% uznaje się za przyzwoite pokrycie), ale zaraz po napisaniu tego postu zabieram się za sporządzenie przynajmniej kilku testów. Trzeba zdążyć przed urlopem
(*) Ta miłość jest nieodwzajemniona, gdy temat rozmowy schodzi na Fixture’y. Pewnie dlatego ten komponent testuje się tak ładnie i szybko – nie korzysta z bazy, tylko z Mock’owanych obiektów.
Testowanie AppModel
Posted by Greg in Agile, CakePHP, TDD in cakePHP on December 16th, 2009
Mam kilka metod w AppModel, potrzebowałem pokryć je testami. Różni się to nieco od testowania zwykłych modeli.
Najpierw analogicznie do innych testów stworzyłem app_model.test.php:
App::import("model", "AppModel");
class AppModelTest extends CakeTestCase {
var $name = "AppModel";
function start() {
parent::start();
$this->AppModel = ClassRegistry::init("AppModel");
}
Problem w tym, że dostaniesz wtedy Missing Database (“app_models”).
Nie możesz w AppModel ustawić atrybutu useTable na false, bo wszystkie modele go dziedziczą. Tzn. jest to możliwe, ale w takim wypadku we wszystkich modelach, które jednak są połączone z tabelami w bazie należałoby explicite podać $useTable = ‘nazwa_tabeli’
Odeszlibyśmy wtedy od Convention over Configuration, poza tym za dużo roboty. Ale można zastosować trick. Stworzyć klasę, która “zasłoni” naszą AppModel, ale będzie działać tak samo (nazywa się to Mock Classes, albo Mock Objects, albo i tak, i tak na pewno
):
App::import("model", "AppModel");
class DummyAppModel extends AppModel {
var $useTable = false;
}
class AppModelTest extends CakeTestCase {
var $name = "AppModel";
function start() {
parent::start();
$this->AppModel = ClassRegistry::init("DummyAppModel");
}
I będzie działać
wbudowana walidacja, a testy jednostkowe modeli
Posted by Greg in CakePHP, TDD in cakePHP on October 30th, 2009
Jeśli piszesz unit testy i polegasz w nich na wbudowanej walidacji – uważaj na pewną drobnostkę.
Załóżmy, że masz model Stuff, a w nim
$validate = array("name" => "notempty");
Przy formularzach dodawania działa to ok, ale teraz chcesz wykonać test:
$this->assertFalse( $this->Stuff->save( array("id"=> 1) ) ); //test fail
Niestety test nie przejdzie (zapis się uda, a nie powinien). Dlaczego?
Otóż taka walidacja jak poniżej oznacza tylko, że
Jeśli w zapisywanych danych jest element “name” to nie może być pusty (null, false, “”).
Zatem, żeby ten zapis nie przeszedł przy tak zdefiniowanej walidacji, należy zrobić:
$this->assertFalse( $this->Stuff->save( array("id"=> 1, "name"=>"") ) ); // test passed
Albo poprawić walidację na
$validate = array("name" => array("rule" =>"notempty", "required"=> true),
);
W takim wypadku zostanie pole zostanie sprawdzone, czy jest niepuste i czy istnieje.
Spędziliśmy tutaj jakieś 30minut zastanawiając się dlaczego przez formularz nie mogę zapisać czegoś nie podając “name”, a w testach jednostkowych się dało.