Archive for category 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.
Możesz podzielić swój kod na moduły w cakePHP
Szybki trick dla dużych projektów: używaj podkatalogów.
Read the rest of this entry »
Własny pingback – kiedy wysłać?
Ktoś, kogo śmiem nazywać “stałym” czytelnikiem tego bloga, czyli Kminek zadał mi ciekawe pytanie :
mam pewna zagwozdke – stworzylem mala platforme blogowa w Cake – cos a`la WordPress.com. przymierzam sie do implementacji protokolu Pingback. jest kwestia wysylania pingow – chyba raczej z punktu widzenia wydajnosci nie mozna tego robic przy zapisywaniu posta tylko uzyc taska shellowego, ktory co pewien czas bedzie to robil? jak sadzisz ?
Myślę, że tak jak sugerujesz, wysłanie pinga nie jest dobrym pomysłem. Sam pisząc posty czasem zapisuję je kilka razy zanim staną się one publicznie dostępne. Gdyby już przy pierwszym zapisie wysłać ping to byłby on wadliwy – prowadziłby do nieistniejącej strony.
Z drugiej strony wspomniany przez Ciebie task shellowy (rozumiem, że chodzi na przykład o crona) jest zadaniem dość kłopotliwym. Moim zdaniem komplikuje on architekturę aplikacji. Oczywiście zależy jak dużo do tego taska chcesz upchnąć? Jeśli parsowanie wpisu w poszukiwaniu linków etc. to moim zdaniem jest to zbyt wiele.
Moim pierwszym pomysłem było wysłanie pinga przy publikacji wpisu. Jednak gdyby w tekście znalazło się 20 różnych linków i wysyłać pingi przy publikacji – mogłoby to zbyt długo trwać i być niewygodne dla autora bloga (wyobrażam sobie sytuację, że przy 18 pingu następuje timeout wykonania skryptu i nie wiadomo czy post się opublikował i czy pingi się udały?).
Dlatego wybrałbym rozwiązanie kombinowane. Tzn. podczas publikowania wpisu (i update’u już opublikowanego) parsowałbym sobie tekst i znalezione linki umieszczałbym w tabeli pingbacks, która mogłaby wyglądać mniej więcej tak:
|---------------------------------------- | pingbacks |---------------------------------------- | id: INT | post_id: INT (albo permalink jeśli wolisz) | uri : varchar (pingowany link) | sent: tinyint(1) DEFAULT 0 |----------------------------------------
oczywiście przed zapisem sprawdzamy, czy już go nie ma w bazie
. Przy takim rozwiązaniu świetnie powinien sprawdzić się Post::afterSave(). Wystarczy sprawdzić, czy zapisany model został z opublikowany i odpalić metodę Post::findAndSavePingbacks($postId).
Mam problem z tym, gdzie umieścić logikę wysyłania pingów. Myślę, że to też należałoby do warstwy Modelu (Pingback::send($limit)). Metoda taka pobrałaby kilka elementów z tabeli `pingbacks`, które nie zostały jeszcze wysłane i po udanym wysłaniu aktualizował pole `sent` (ustawiając 1).
Na koniec w kontrolerze zdefiniowałbym akcję, która miałaby za zadanie jedynie odpalić metodę Pingbacks::send()
class PingbacksController extends AppController{
function send($limit=10){
$this->autoRender = false;
$this->Pingback->send($limit);
}
}
I teraz jeśli mamy dostęp do crona to możemy zdefiniować na przykład akcję wget http://example.com/pingbacks/send
Jeśli nie mamy dostępu do takich luksusów – możemy wzbudzać tą akcję np. w AppController::afterFilter().
Co myślicie o tym rozwiązaniu?