PHP Magazyn – Mocki, Stuby jak i kiedy korzystać Written on Grudzień 1, 2011, by wookieb.
Na łamach PHPMagazyn ukazał się już mój drugi artykuł o testach jednostkowych. Tym razem opisuję jak działają mocki oraz stuby w PHPUnit. Po co z nich korzystać, kiedy i dlaczego są takie ważne. Jeżeli umiesz tworzyć testy jednostkowe oprogramowania a nie do końca wiesz jak korzystać z mocków ten artykuł jest dla Ciebie.
Artykuł http://phpmagazyn.pl/media/download/phpmagazyn2.pdf
Przykładowy kod źródłowy z testami https://github.com/wookieb/UnitTest-Article-Part-2
Read more from the Bez kategorii category. If you would like to leave a comment, click here: Comment. or stay up to date with this post via RSS, or you can
Trackback from your site.
Oszczędność pamięci przy pracy na dużych zbiorach danych dzięki iteratorom Written on Sierpień 12, 2011, by wookieb.
Za każdym razem kiedy pracujemy z duża ilością rekordów w jednej pętli, liczymy na to, że nie skończy się pamięć. Niektórzy nie zaprzątają sobie tym głowy – do czasu… Kiedy usługa nabiera kształtu i rozmiaru, a w tle uruchamiają się procesy, które co chwilę plują błędami o przekroczeniu limitu pamięci, zaczyna się czuć niezły zapach… problemu w jakim się znaleźli.
Kiedy zwiększenie limitu pamięci nie przyniesie zamierzonego efektu sięgamy po starą dobrą metodę paczkowania danych. Tyle ile razy widziałem realizację owej metody, tyle razy brało mnie na mdłości. Jest znacznie prostsze i o niebo estetyczniejsze rozwiązanie. Iterator. A dokładniej iterator, który sam zadba o paczkowanie danych.
Na początek definicja przykładowego (wersja z PDO, znacznie uproszczona – czytelnik sam powinien zadbać o dostosowanie iteratora pod swój system).
class Iterator_DataPartition implements Iterator {
/**
* @var PDO
*/
protected $_pdo;
/**
* Num of records per partition
* @var integer
*/
protected $_partitionSize;
/**
* @var integer
*/
private $_position = 0;
/**
* Actual partition num
* @var integer
*/
private $_partitionNum = 0;
/**
* Actual partition records
* @var array
*/
protected $_data = array();
/**
* @param PDO $pdo
* @param integer $partitionSize num of records per package
* @param mixed fetchParams...
*/
public function __construct(PDO $pdo, $query, $partitionSize = 50) {
$this->_pdo = $pdo;
$this->_checkQuery($query);
$this->_query = $query;
$this->_partitionSize = (int)$partitionSize;
}
protected function _checkQuery(&$query) {
$query = trim($query);
if (!$query) {
throw InvalidArgumentException('Query is empty');
}
}
public function current() {
return $this->_data[$this->_position];
}
public function key() {
return $this->_partitionNum * $this->_partitionSize + $this->_position;
}
public function next() {
$this->_position++;
}
public function rewind() {
$this->_loadPartition(0);
$this->_position = 0;
}
public function valid() {
if (isset($this->_data[$this->_position])) {
return true;
}
if ($this->_position < $this->_partitionSize) {
return false;
}
if ($this->_loadPartition(++$this->_partitionNum)) {
$this->_position = 0;
return true;
}
return false;
}
/**
* Load records for given partition number
*
* @param type $numOfPartition
* @return boolean whether records found
*/
protected function _loadPartition($numOfPartition) {
$this->_partitionNum = (int)$numOfPartition;
$query = $this->_getPartitionQuery();
$stmt = $this->_pdo->query($query);
if ($stmt) {
$this->_data = $stmt->fetchAll();
}
return (bool)$this->_data;
}
/**
* Create query for fetching partitiokn records
* @return string
*/
protected function _getPartitionQuery() {
$offset = $this->_partitionNum * $this->_partitionSize;
$limit = $this->_partitionSize;
return $this->_query.' LIMIT '.$limit.' OFFSET '.$offset;
}
}
Przykład użycia
$pdo; // nasza przygotowana instancja PDO
$iterator = new Iterator_DataPartition($pdo, 'SELECT * FROM test');
foreach ($iterator as $record) {
// obróbka rekordu
}
Prawda, że wygodne? Zasada działania jest niezwykle prosta. Przy końcu iteracji aktualnej paczki, próbuje pobrać następną. Dzięki temu zużywamy maksymalnie tyle pamięci jak duża jest paczka rekordów.
Sprawdźmy jak wygląda w praktyce zużycie pamięci przy standardowym pobraniu rekordów a przy użyciu naszego iteratora.
Tabela ‘test’ przedstawia się następująco.
CREATE TABLE `test` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) COLLATE utf8_unicode_ci NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=7292 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
Wypełniłem ją 7291 rekordami gdzie jako „name” wstawiłem „Test name”.
Standardowa iteracja po wszystkich rekordach.
$pdo; // nasze PDO
$startTime = microtime(true);
$initMemory = memory_get_usage();
$memory = 0;
foreach ($pdo->query('SELECT * FROM test') as $record) {
$memory = max($memory, memory_get_usage());
}
echo ($memory-$initMemory).PHP_EOL;
echo (microtime(true) - $startTime);
Zużycie pamięci: 562552
Czas wykonywania: 0.23115491867065
A teraz Iterator_DataPartition
// mierzenie pamięci i czasu identycznie jak wyżej
$pdo; // nasze PDO
$iterator = new Iterator_DataPartition($pdo, 'SELECT * FROM test');
foreach ($iterator as $record) {
$memory = max($memory, memory_get_usage());
}
Zużycie pamięci: 25896
Czas wykonywania: 0.93066096305847
Ponad 20 krotnie mniejsze zużycie pamięci! Wynik jest naprawdę dobry ![]()
Niestety za to czas wykonywania wzrósł około czterokrotnie. W przypadku takiego małego zbioru danych różnica jest niewielka lecz przy większych zbiorach problem będzie narastać.
Zoptymalizujmy trochę Iterator. Zanim to zrobimy poznajmy przyczynę problemu. Jest nią nic innego jak wykonywanie zapytania z dużym offsetem. Mysql (jak i zresztą inne bazy danych) potrzebują znacznie więcej czasu na wykonanie takiego zapytania, ponieważ baza musi znaleźć najpierw OFFSET + LIMIT rekordów a następnie odrzucić wszystkie OFFSET pierwszych. Im większy offset, tym dłużej trwa wykonywania zapytania. Ale jest na to rozwiązanie.
Partycjonowanie danych po ID.
Przypominam, że to tylko iterator poglądowy. Właściwa wersja docelowa zależy od waszej implementacji.
class Iterator_DataPartition_IdOffset extends Iterator_DataPartition {
private $_maxId = 0;
const ID_VALUE_TOKEN = '[ID_VALUE]';
protected function _checkQuery(&$query) {
parent::_checkQuery($query);
if (strpos($query, self::ID_VALUE_TOKEN) === false) {
throw new InvalidArgumentException('No token '.self::ID_VALUE_TOKEN.' in given query');
}
}
protected function _loadPartition($numOfPartition) {
$return = parent::_loadPartition($numOfPartition);
if ($return) {
$lastRecord = end($this->_data);
$this->_maxId = $lastRecord['id'];
}
return $return;
}
protected function _getPartitionQuery() {
return str_replace(self::ID_VALUE_TOKEN, $this->_maxId, $this->_query).' LIMIT '.$this->_partitionSize;
}
}
Ta wersja iteratoa wykonuje zapytanie według schematu:
SELECT * FROM tabela WHERE id > [OSTATNIE_ID_REKORDU_Z_OSTATNIEJ_PACZKI] ORDER BY id LIMIT [ROZMIAR_PARTYCJI]
P.s. „ORDER BY id” w wypadku prostego zapytania, ale zwracam na to uwagę abyście wiedzieli, że wyniki MUSZĄ być odpowiednio posortowane.
Mysql ma znacznie mniej roboty. Wyszukuje rekordy gdzie id jest większe od X (które jest największym ID z ostatniej paczki). Z racji tego, że ID to najczęściej nasz index (do tego główny) więc takie zapytanie wykona się znacznie znacznie szybciej. Dodatkowo ograniczamy wyniki do ROZMIAR_PARTYCJI rekordów.
Sprawdźmy jak się spisuje.
// mierzenie pamięci i czasu identycznie jak wyżej
$memory = 0;
$iterator = new Iterator_DataPartition_IdOffset($pdo, 'SELECT * FROM test WHERE id > [ID_VALUE]', 5);
foreach ($iterator as $record) {
$memory = max($memory, memory_get_usage());
}
Zużycie pamięci: 26024
Czas wykonywania: 0.6228621006012
Co prawda czas nadal nie jest „super” aczkolwiek to jedyne opcje optymalizacji jakie znam. Jeżeli zwiększymy rozmiar paczki iteracja będzie szybsza (mniej zapytań do wykonania), ale zużycie pamięci wzrośnie. Dlatego niezwykle ważne jest wybranie odpowiedniego rozmiaru paczki w zależności od naszych potrzeb.
Na koniec tabelka podsumowująca:
| / | Standardowe pobieranie danych | Iterator_DataPartition | Iterator_DataPartition_IdOffset |
|---|---|---|---|
| Zużycie pamięci | 562552 | 25896 | 26024 |
| Czas iteracji: | 0.23115491867065 | 0.93066096305847 | 0.6228621006012 |
Read more from the Bazy Danych, PHP category. If you would like to leave a comment, click here: Comment. or stay up to date with this post via RSS, or you can
Trackback from your site.
Obsługa błędów w Node.js za pomocą zdarzeń Written on Sierpień 6, 2011, by wookieb.
Obsługa wyjątków w JavaScriptcie nie zawsze jest taka oczywista jak by się mogło wydawać. Weźmy pod uwagę następujący przykład.
try {
setTimeout(function timeoutException() {
throw new Error('Name');
}, 100);
} catch (e) {
console.log('Got IT!'); // this will never happen
}
Okaże się, że wyjątek wyrzucony w funkcji timeoutException nie zostanie przechwycony przez nasz blok „try” z jednej prostej przyczyny. Został wyrzucony w funkcji wywoływanej asynchronicznie. Zachowanie nie było dla mnie do końca oczywiste (tak zbijcie mnie) do czasu naświetlenia sytuacji przez Kasię Drzyzgę która to wyjaśniła mi, że gdyby podany przypadek miał zadziałać tak jakbym chciał (czyli wyjątek został prawidłowo przechwycony), blok „try” musiałby oczekiwać na zakończenie timeoutException, co oczywiście jest niedorzeczne (aczkolwiek teoretycznie możliwe). Nie pozostaje nam nic innego jak przechwytywać tego typu wyjątki w samym źródle problemu, czyli owych problematycznych funkcjach.
Takie rozwiązanie nie satysfakcjonuje mnie do końca. Szczególnie w kontekście Node.js gdzie tego typu funkcje są na porządku dziennym. A zadbanie o odpowiednią obsługę błędów w każdej z nich spowodowałby dodanie kolejnej anomalii kodu a tych już wystarczy w aplikacjach na Node.js. Niestety nie mamy wyboru, więc jak ułatwić sobie zadanie?
Z pomocą przychodzą niedoceniane zdarzenia.
var EventEmitter = require('events').EventEmitter;
var _ = require('underscore');
var EventEmitterError = function() {
var emitter = new EventEmitter();
emitter.error = function(error, code) {
this.emit('error', error, code);
}
emitter.error = _.bind(emitter.error, this);
_.extend(this, emitter);
}
// Szybsza droga do wzbogacenia podanego obiektu o naszego "ErrorHandlera"
EventEmitterError.enchant = function(object) {
_.extend(object, new EventEmitterError());
}
module.exports = EventEmitterError;
Uzyskaliśmy w ten sposób rozszerzoną wersję EventEmittera, który to zakłada, że za każdym razem kiedy wystąpi błąd wykorzystamy metodę error do poinformowania o takiej sytuacji. Złapanie takiego błędu wymaga jedynie zdefiniowania nasłuchiwacza na zdarzenie „error”. Żadnych wyjątków bo znowu powrócimy do mozolnego wklepywania Try/Catch.
Proste przykłady użycia.
// goły obiekt, mało przydatny ale jeżeli komuś potrzebny...
var emitter = new EventEmitterError();
emitter.on('error', function(error) {
console.log('Got it: '+error);
});
emitter.error('Fire <exclamation mark> Fire <exclamation mark>');
// Użycie z konkretnym obiektem
var apollo = {
start: function() {
this.error('Houston. We have a problem!');
}
}
EventEmitterError.enchant(apollo);
apollo.on('error', function(error) {
console.log('Easy easy: '+error);
});
apollo.start();
Teraz obsługując żądanie HTTP możemy obsłużyć wszelkie błędy w następujący sposób.
var Http = require('http');
var Emitter = require('EventEmitterError');
var Controller = require('OurSomeController');
var server = Http.createServer(function(request, response){
var controller = new Controller();
Emitter.enchant(controller);
controller.on('error', function(error) {
response.statusCode = 500;
response.end(error);
});
controller.dispatch(request, function(result) {
response.end(result);
});
}).listen(80);
Przykładowy kontroler – żeby nie było wątpliwości co do użycia powyższego rozwiązania.
var Sha1File = require('sha1_file');
module.exports = function() {
var controller = this;
this.dispatch = function(request, callback) {
Sha1File('/some/file', function(error, checksum){
if (error) {
controller.error(error);
return;
}
callback(checksum);
});
}
}
Pozostał do oznajmienia jeden niuans.
Standardowo EventEmitter lekko podszprycowuje obsługę zdarzenia „error” do tego stopnia iż w przypadku wystąpienie owego zdarzenia i braku jakichkolwiek nasłuchiwaczy na nie, zakończy działanie Node.js i wyrzuci błąd. Jeżeli nie akceptujecie takiego zachowania po prostu użyjcie innej nazwy niż „error” dla zdarzenia błędu.
Czy takie rozwiązanie wam odpowiada?
Dodatkowo odsyłam do innej (jeszcze nie wdrożonej) propozycji wyłapywania asynchronicznych wyjątków.
Read more from the SSJS category. If you would like to leave a comment, click here: 5 Comments. or stay up to date with this post via RSS, or you can
Trackback from your site.
Prywatne metody w coffeescript Written on Lipiec 29, 2011, by wookieb.
Klasy w js? Oczywiście, że coś takiego nie istnieje ale coffeescript stara się dodać coś podobnego.
Wyszło całkiem ładnie poza metodami prywatnymi. Nie wszyscy „wyznawcy jsitsu” tolerują istnienie metod prywatnych ale dla mnie są niezbędne.
Przejdźmy do rzeczy. Przykładowa klasa z prywatną metodą.
class TestClass privateMethod= -> console.log 'private' publicMethod: -> console.log 'public' privateMethodCall: -> privateMethod() cl = new TestClass cl.publicMethod() # output: public cl.privateMethodCall(); # output: private cl.privateMethod() # Error! No method "privateMethod"
Cool. Zamieniliśmy znak „:” na „=” co powoduje, że nie definiujemy metody publicznej klasy (według założenia CoffeeScript) a jedynie zmienną o nazwie „privateMethod” będącej w zasięgu definicji metod publicznych, która jest funkcją.
Jak teraz wywołać naszą prywatną metodę w kontekście aktualnego obiektu?
Zapewne wielu pokusi się o zapis „=>” zamiast „->” przy definicji privateMethod.
class TestClass privateMethod= => console.log @name constructor: -> @name = 'test' testName: => console.log @name privateMethodCall: => privateMethod() cl = new TestClass cl.testName() # Output: test cl.privateMethodCall() # Output: undefined
Cóż się stało? Teoretycznie zrobiliśmy wszystko ok. Niestety CoffeeScript nie rozumie takiego zapisu tak jak byśmy chcieli.
Zbindowanie privateMethod za pomocą => powinno z poziomu JS wykonać się w konstruktorze. W rzeczywistości wygląda to mniej więcej w ten sposób.
var privateMethod;
privateMethod = __bind(function() {
return console.log(this.name);
}, TestClass);
Niestety, metoda nie została zbindowana z nowa instancją obiektu ale z definicją funkcji TestClass. Nie tego chcemy.
Zatem musimy ręcznie „analogowo” zadbać o kontekst. Jeżeli nie chcemy wypruwać żył przy pisaniu funkcji do bindowania i np używamy underscore-a możemy to zrobić następująco.
class TestClass privateMethod= => console.log @name constructor: -> privateMethod = _.bind privateMethod, this @name = 'test' testName: => console.log @name privateMethodCall: => privateMethod() cl = new TestClass cl.testName() # Output: test cl.privateMethodCall() # Output: test
Teraz jest wszystko ok.
Jeżeli twą naturalną misją apostolską jest „NO UNDERSCORE” i chcemy wykorzystać „wewnętrzną” funkcję __bind, którą wykorzystuje coffeescript napotka nas kolejna niespodzianka.
CoffeeScript uzna __bind jako słowo zarezerwowane i nie możemy go użyć. Phrum… Ja nie mogę?
Wykorzystajmy możliwość pisania czystego JS w samym Coffee. Jeżeli umieścimy nasz kod w ` `, nie zostanie on przeparsowany przez CoffeeScript tylko wrzucony takim jaki wpisaliśmy. A zatem:
class TestClass `var bind = __bind` privateMethod= => console.log @name constructor: -> privateMethod = bind privateMethod, this @name = 'test' testName: => console.log @name privateMethodCall: => privateMethod() cl = new TestClass cl.testName() # Output: test cl.privateMethodCall() # Output: test
A teraz odpowiedzmy sobie na pytanie. Czy narzędzie, które MIAŁO ułatwiać pisania aplikacji w JS powinno tworzyć TAKIE utrudnienia? Póki co poczekajmy na rozwój narzędzia, ale osobiście nie jestem do niego przekonany.
Read more from the CoffeeScript category. If you would like to leave a comment, click here: 2 Comments. or stay up to date with this post via RSS, or you can
Trackback from your site.
Exception jako zastepca wielopoziomowego return Written on Lipiec 27, 2011, by wookieb.
Ileż to razy męczyliście się z masą returnów w funkcji albo też kontrolą zwracanych wartości funkcji „podrzędnych”?
Przykładowo posiadamy metodę Router-a, która ma sprawdzić parę warunków dla konkretnej reguły routingu oraz zwrócić dopasowane parametry. Zgodnie z zasadami clean code powinniśmy podzielić zadanie mniej więcej w ten sposób:
public function matchRoute(Router_Route $route) {
$result = $this->_matchSite($route);
if ($result) {
return $result;
}
$result = $this->_matchMainPage($route);
if ($result) {
return $result;
}
$result = $this->_matchRoutesByStaticPrefix($route);
if ($result) {
return $result;
}
$result = $this->_matchRoutesByDynamicPrefix($route);
if ($result) {
return $result;
}
$result = $this->_matchUrlToResource($route);
if ($result) {
return $result;
}
}
Każda metoda sprawdza warunki konkretnego typu (nieważne jakie). Zasada działania dosyć jasna, ale kod jest nie dość czytelny.
Upiększmy go trochę.
Stwórzmy wyjątek o nazwie BreakException, który to będzie oznaczał konieczność przerwania dalszego wykonywania metody/bloku metod.
/**
* Kind of mark end of execution "try" block
*
* @author wookieb
* @package Exception
*/
class BreakException extends Exception {
}
Teraz nasz kod może wyglądać w ten sposób.
public function matchRoute(Router_Route $route) {
try {
$this->_matchSite($route);
$this->_matchMainPage($route);
$this->_matchRoutesByStaticPrefix($route);
$this->_matchRoutesByDynamicPrefix($route);
$this->_matchUrlToResource($route);
}
catch (BreakException $e) {
// wlasciwosc "result" ustawiamy we wnetrzu powyzszych metod
return $e->result;
}
}
Metody objęte blokiem „try” wyrzucają wyjątek gdy wykryją, że reguła została spełniona i nie ma sensu wykonywać dalszych kontroli poprawności. Super, sieć IF wywalona.
Jednakże co wtedy jeżeli jedna z metod sprawdzających uzna, że reguła routingu kompletnie nie pasuje ani do niej, ani do innych warunków? Chcemy wtedy definitywnie zakończyc wykonywanie matchRoute i zwrócić stosowna odpowiedz. Nadal możemy wykorzystać BreakException (wlaąciwoąć $result ustawimy na false) ale proponuję inne rozwiązanie.
Dorzućmy dwa nowe wyjątki PassException oraz FailException dziedziczące po BreakException.
/**
* @author wookieb
* @package Exception
*/
class PassException extends BreakException {
}
/**
* @author wookieb
* @package Exception
*/
class FailException extends BreakException {
}
Zmieńmy nasze matchRoute
public function matchRoute(Router_Route $route) {
try {
$this->_matchSite($route);
$this->_matchMainPage($route);
$this->_matchRoutesByStaticPrefix($route);
$this->_matchRoutesByDynamicPrefix($route);
$this->_matchUrlToResource($route);
}
catch (PassException $e) {
// siakies konkretne wyniki
return $e->result;
}
catch (FailException $e) {
// tutaj inny fikusny kod, np logowanie bledu
// ...
return false;
}
}
Teraz jasno (i jak elegancko) widzimy kiedy kończymy dopasowywanie powodzeniem a kiedy porażka.
Niestety jest jedno małe „ale” z pokryciem kodu dla owego fragmentu podczas testów jednostkowych. Otóż jeżeli nawet przetestujemy całą metodę dość dokładnie, może się okazać, że klamry zamykające bloki catch zostaną oznaczone jako niewykonany/nieprzetestowany kod. Oczywiście teoretycznie to bzdura ale xdebug jest niemiłosierny. Obejdźmy to w nastepujący sposób.
public function matchRoute(Router_Route $route) {
$result = null;
try {
$this->_matchSite($route);
$this->_matchMainPage($route);
$this->_matchRoutesByStaticPrefix($route);
$this->_matchRoutesByDynamicPrefix($route);
$this->_matchUrlToResource($route);
}
catch (PassException $e) {
// siakies konkretne wyniki
$result = $e->result;
}
catch (FailException $e) {
// tutaj inny fikusny kod, np logowanie bledy
// ...
$result = false;
}
return $result;
}
Teraz możecie być dumni z waszego zastępcy wielu „ifów”
Read more from the Triki programistyczne category. If you would like to leave a comment, click here: 4 Comments. or stay up to date with this post via RSS, or you can
Trackback from your site.


