Archive for category TDD in cakePHP
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.
TDD na żywym organiźmie
Posted by Greg in Agile, TDD in cakePHP on October 27th, 2009
Jakiś czas temu próbowałem nauczyć się stosowania TDD na “testowym” projekcie. Niestety testowanie kontrolerów i widoków wydało mi się zbyt problematyczne.
Pod wpływem zgłębianiu tematu przy okazji pisania pracy magisterskiej zacząłem też myśleć o tym, że pokrycie kodu testami w 100% może nie być możliwe – szczególnie w przypadku, gdy technika ta nie jest znana zespołowi. Jako, że podoba mi się podejście “sztuki rzeczy możliwych” – pomyślałem, że przynajmniej trzeba mieć unit testy w miejscach krytycznych. Zakładając, że stosujemy uczciwie podejście fat model, skinny controller – większość krytycznych elementów znajduje się właśnie w modelach.
Modele łatwiej jest testować niż “wyższe” warstwy modeli MVC, więc tym bardziej wspomniana zasada wydaje się być ważna.
Ostatnio w projekcie natknęliśmy się na ciekawy przypadek. W bazie istniały obiekty, do których mogli być przypisani właściciela (wiele właścicieli do wielu obiektów):
Object habtm Owner
Dodatkowo to przypisanie było ograniczone czasowo, tzn.:
Object1 jest powiązany z OwnerA od 15 marca 2009 do 31 lipca 2009
oraz
OwnerB jest powiązany z Object1 od 01 czerwca do nie_wiadomo_kiedy (to znaczy, że aktualnie jest przypisany, ale nie wiadomo, kiedy ten stan się zakończy, nazwę to przypisaniem otwartym na potrzeby tego wpisu).
Możliwa była sytuacja, gdy Owner1 jest przypisany do ObjectA tak jak w przykładzie powyżej i od 01 listopada do nie_wiadomo_kiedy też z ObjectA.
Nie zagłębiając się więcej w szczegóły dodam tylko, że potrzebna była metoda, która odpowie na pytanie “czy dla danego Object, Owner i danych dat początkowej i końcowej mogę dodać powiązanie?”.
Członek zespołu zabrał się za to zadanie i po jakimś czasie skończył. Jednak wyznał szczerze, że nie jest pewien, czy ten fragment dobrze działa. Problem zdawał się rosnąć w momencie używania przedziałów otwartych z prawej strony. Pomyślałem, żeby zamiast ślęczeć nad kodem przez najbliższą godzinę i ręcznie testować przypadki – pobawić się w TDD.
Może nie bardzo podobało się to programistce, która nad kodem aktualnie pracowała, ale kod ten został wywalony – łatwiej jest stosować TDD gdy zaczyna się od zera (przynajmniej na moim, bardzo początkującym poziomie). Pracowaliśmy od teraz wspólnie nad kodem.
Podejście było takie, żeby dodawać coraz to kolejne kombinacje testów – dodanie powiązania z konkretnymi datami gdy w bazie istnieje przypisanie zamknięte, próba dodania przypisania otwartego gdy w bazie jest zamknięte i wszystkie możliwe kombinacja. Wyszło ich około 20*.
Proces wyglądał następująco:
1. test przypadku
2. fail testu
3. poprawa testowanej funkcji
4. pass wszystkich testów
5. test dla następnego przypadku
6. fail testu
Gdy była możliwość dokonywany był refaktoring testowanej funkcji, a raz nawet refaktoring samych testów.
Jaki był bilans tego działania? Pomyślmy jak wyglądałoby życie, gdybyśmy podeszli do tego problemu “po staremu”
Zajęło by to jakąś godzinę, mielibyśmy “dość mocne przekonanie”, że metoda spełnia wymagania. Jednak raczej staralibyśmy się uniknąć dotykania tego fragmentu w przyszłości, bo to oznaczałoby przeprowadzenie testów ręcznych od początku.
Nie posiadalibyśmy jednak twardych dowodów na to, że wszystkie testy zostały przeprowadzone – tylko silne poczucie, że “raczej tak”.
Jak to wyglądało z TDD?
Zajęło to około 4 godzin. Mamy twarde dowody na to, że wszystkie przypadki, które wymyśliliśmy są testowane. Mogą być przetestowane w każdym momencie. Znaleźliśmy błąd polegający na niesprawdzaniu, czy data startowa powiązania jest mniejsza niż końcowa. W efekcie tych działań otrzymaliśmy bardzo przyjemną funkcję logiczną, którą z przyjemnością wrzuciliśmy do funkcji beforeValidate testowanego modelu, aby żaden zapis nie mógł się odbyć przy nieodpowiednich danych.
Gdyby klient zaskoczył nas zmianą wymagań dot. zasad, które zaimplementowaliśmy – bez większych problemów można zabrać się za ich modyfikację pilnując, żeby dotychczasowe testy, które jeszcze są aktualne, przechodziły.
Jako bonus – jeden z członków zespołu dowiedział się czegoś więcej o TDD.
Dlatego jeśli myślisz, że TDD jest przerażające, bo wymaga pisania testów wszędzie (jak ja myślałem na początku), to możesz spróbować stosowania TDD tylko w krytycznych miejscach. Najczęściej w modelach. Kto wie, może po jakimś czasie, gdy oswoimy się z tą techniką pojawi się jakaś koncepcja, żeby zastosować TDD w kontrolerach a później widokach?