Development

Documentation/pl_PL/book/1.0/15-Unit-and-Functional-TestingChapter

You must first sign up to be able to contribute.

Version 135 (modified by Waldemar.Mekal, 10 years ago)
Translation Review

Oryginalny tekst: http://www.symfony-project.com/book/trunk/15-Unit-and-Functional-Testing [EN]

WERSJA ROBOCZA

Rozdział 15 - Testowanie jednostkowe i funkcjonalne

Zautomatyzowane testy są jednym z największych postępów w programowaniu od czasu podejścia obiektowego. Mogą zagwarantować jakość aplikacji nawet jeżeli jej wydania są liczne, co szczególnie sprzyja rozwijaniu aplikacji web. Symfony dostarcza wielu narzędzi w celu ułatwienia automatycznego testowania. Są one przedstawione w niniejszym rozdziale.

Zautomatyzowane testy

Każdy programista posiadający doświadczenie w tworzeniu aplikacji web jest świadom czasu poświęconego na ich dobre przetestowanie. Pisanie testów, ich uruchamianie i analiza rezultatów jest żmudną pracą. Na dodatek wymagania w stosunku do aplikacji web zmieniają się ciągle, co prowadzi do kolejnych wydań i ciągłej potrzeby refaktoryzowania kodu. W tym kontekście istnieje duże prawdopodobieństwo regularnego pojawiania się nowych błędów.

Z tego powodu zautomatyzowane testy są wskazanym, jeśli nie wymaganym, składnikiem dobrego środowiska tworzenia oprogramowania. Zbiór testów może zagwarantować, że program faktycznie robi to, co powinien. Nawet jeśli często dokonuje się zmian w kodzie, zautomatyzowane testy zapobiegają przypadkowym regresjom. Dodatkowo, zmuszają programistów do pisania testów w standardowym, ustalonym formacie umożliwiającym ich zrozumienie przez framework testujący.

Testy są czasami w stanie zastąpić dokumentację techniczną, ponieważ mogą w jasny sposób obrazować, co aplikacja powinna robić. Dobry zestaw testów pokazuje, jakie wyniki testów są oczekiwane dla określonego zbioru wartości wejściowych, co jest znakomitym sposobem na wyjaśnienie przeznaczenia metody.

Zasada ta jest stosowana w Symfony. Działanie framework'a jest walidowane za pomocą zautomatyzowanych testów jednostkowych i funkcjonalnych. Nie są one dołączone w standardowej dystrybucji Symfony, ale można je ściągnąć z repozytorium SVN lub przejrzeć online pod adresem [http://www.symfony-project.com/trac/browser/trunk/test].

Testy jednostkowe i funkcjonalne

Testy jednostkowe służą do potwierdzenia, że jednostkowy element kodu zwraca poprawny wynik dla zadanej wartości wejściowej. Sprawdzają jak funkcje i metody działają w każdym szczególnym przypadku. Jeden test jednostkowy dotyczy tylko jednego przypadku, więc jedna metoda może wymagać wielu testów jednostkowych, jeżeli działa inaczej w określonych sytuacjach.

Testy funkcjonalne nie sprawdzają prostych zależności wejście-wyjście, lecz daną funkcjonalność. Dla przykładu, system cache może być sprawdzony tylko za pomocą testu funkcjonalnego, ponieważ zawiera więcej niż jeden krok: przy pierwszym żądaniu strona jest generowana, za drugim - pobierana z cache. Więc testy funkcjonalne walidują dany proces i wymagają scenariusza. W Symfony powinieneś pisać testy funkcjonalne dla wszystkich swoich akcji.

W przypadku bardziej skomplikowanych interakcji, powyższe sposoby mogą okazać się niewystarczające. Na przykład operacje z użyciem Ajax wymagają przeglądarki internetowej do wykonywania Javascript, więc automatyczne testowanie wymaga dodatkowego narzędzia. Ponadto efekty wizualne mogą być walidowane tylko przez człowieka.

Jeżeli masz kompleksowe podejście do automatycznego testowania, prawdopodobnie będziesz musiał wykorzystać kombinację wszystkich przedstawionych metod. Wskazówka: pamiętaj aby testy były proste i czytelne.

NOTE Zautomatyzowane testy działają na zasadzie porównania wyników otrzymanych z oczekiwanymi. Innymi słowy wykonują one asercje ( wyrażenia jak $a == 2 ). Asercja może przyjmować wartość true lub false i określa ona, czy test zakończył się pomyślnie. Słowo "asercja" jest powszechnie używane w związku z technikami zautomatyzowanego testowania.

Programowanie sterowane testami (Test Driven Development)

W metodologii programowania sterowanego testami (TDD) testy są pisane przed kodem. Pomaga to skoncentrować się na zadaniach, jakie funkcja powinna realizować jeszcze przed jej implementacją. To dobra praktyka, która jest zalecana również przez inne metodologie, np. Extreme Programming (XP). Uwzględnia ponadto niezaprzeczalny fakt, że jeżeli nie napiszesz testów na początku, nie napiszesz ich nigdy.

Dla przykładu wyobraź sobie, że musisz napisać funkcję przetwarzającą tekst. Funkcja usuwa białe znaki z początku i końca łańcucha, zastępuje znaki spoza alfabetu znakami podkreślenia, a także zamienia wszystkie duże litery na małe. W programowaniu sterowanym testami pomyślałbyś najpierw o wszystkich możliwych przypadkach i zapewniłbyś przykładowe wartości wejściowe i oczekiwane wartości wyjściowe dla każdego, jak pokazano w tabeli 15-1.

Tabela 15-1 - Lista przypadków testowych dla funkcji przetwarzającej tekst

Wejście | Oczekiwane wyjście --------------------- | --------------------- " foo " | "foo" "foo bar" | "foo_bar" "-)foo:..=bar?" | "__foo____bar_" "FooBar" | "foobar" "Don't foo-bar me!" | "don_t_foo_bar_me_"

Napisałbyś testy, uruchomił je i zobaczył, że żaden nie przeszedł. Następnie dodałbyś kod niezbędny do obsłużenia pierwszego przypadku, uruchomił testy znowu, zobaczył, że pierwszy test przeszedł i postępował dalej w ten sposób. W końcu, kiedy wszystkie testy kończą się powodzeniem, funkcja działa poprawnie.

Aplikacja stworzona z wykorzystaniem metodologii TDD zawiera w przybliżeniu tyle samo kodu testującego, co kodu programu. Jeżeli nie chcesz poświęcać swojego czasu na debugowanie testów, staraj się aby były proste.

NOTE Refaktoryzacja metody może skutkować powstaniem błędów, wcześniej nie występujących. Dlatego dobrym nawykiem jest uruchamianie zautomatyzowanych testów przed umieszczeniem nowej wersji aplikacji w środowisku produkcyjnym - nazywa się to testowaniem regresyjnym.

Framework testujący Lime

Istnieje wiele framework'ów służących do tworzenia testów jednostkowych w PHP, jak chociażby najbardziej znane PhpUnit i SimpleTest. Symfony posiada własny, zwany Lime. Opiera się on na bibliotece języka Perl Test::More i jest zgodny z TAP, co oznacza, że wyniki testów są wyświetlane tak, jak określono to w protokole Test Anything Protocol, zaprojektowanym w celu uzyskania lepszej czytelności wyników testów.

Lime zapewnia wsparcie dla testowania jednostkowego. Jest o wiele lżejszy od innych framework'ów testujących i ma wiele zalet:

  • Uruchamia pliki testów w "piaskownicy", aby zapobiec dziwnym efektom ubocznym pomiędzy uruchomieniami kolejnych testów. Nie wszystkie framework'i gwarantują jednakowe środowisko dla każdego testu.
  • Testy Lime są bardzo czytelne, podobnie jak ich wyświetlane wyniki. Na kompatybilnych systemach, Lime wykorzystuje kolorowe wyjście, aby w inteligentny sposób wyróżnić ważne informacje.
  • Symfony używa testów Lime do testowania regresyjnego, więc wiele przykładów testów jednostkowych i funkcjonalnych można znaleźć w kodach źródłowych Symfony.
  • Jądro Lime jest walidowane za pomocą testów jednostkowych.
  • Jest napisany w PHP, jest szybki oraz dobrze napisany. Składa się z jednego pliku, lime.php, bez żadnych zależności.

Testy opisane w niniejszym rozdziale używają składni Lime. Działają z każdą instalacją Symfony.

NOTE Niewskazane jest uruchamianie testów jednostkowych i funkcjonalnych w środowisku produkcyjnym. Są one narzędziami programisty, więc powinny być uruchamiane na jego komputerze, a nie na serwerze hosta.

Testy jednostkowe

Testy jednostkowe w Symfony są prostymi plikami PHP o nazwach kończących się na Test.php, umieszczonymi w katalogu test/unit/ twojej aplikacji. Posiadają prostą i czytelną składnię.

Jak wyglądają testy jednostkowe?

Listing 15-1 przedstawia typowy zestaw testów jednostkowych dla funkcji strtolower(). Zaczyna się inicjalizacją obiektu lime_test (użyte parametry nie są w tej chwili istotne). Każdy test jest wywołaniem pewnej metody obiektu lime_test. Ostatnim parametrem każdej z tych metod jest opcjonalny łańcuch służący za komentarz.

Listing 15-1 - Przykład testów jednostkowych, zawartych w pliku test/unit/strtolowerTest.php

[php]
<?php

include(dirname(__FILE__).'/../bootstrap/unit.php');
require_once(dirname(__FILE__).'/../../lib/strtolower.php');

$t = new lime_test(7, new lime_output_color());

// strtolower()
$t->diag('strtolower()');
$t->isa_ok(strtolower('Foo'), 'string',
    'strtolower() zwraca typ string');
$t->is(strtolower('FOO'), 'foo',
    'strtolower() zamienia duże litery na małe');
$t->is(strtolower('foo'), 'foo',
    'strtolower() pozostawia małe litery niezmienione');
$t->is(strtolower('12#?@~'), '12#?@~',
    'strtolower() pozostawia znaki spoza alfabetu niezmienione');
$t->is(strtolower('FOO BAR'), 'foo bar',
    'strtolower() pozostawia odstępy niezmienione');
$t->is(strtolower('FoO bAr'), 'foo bar',
    'strtolower() radzi sobie z literami o różnej wielkości');
$t->is(strtolower(''), 'foo',
    'strtolower() zamienia puste łańcuchy w napis foo');

Uruchom zbiór testów z linii poleceń za pomocą polecenia test-unit. Wyniki wyświetlane w konsoli są bardzo czytelne i pomagają określić, które testy zakończyły się pomyślnie, a które nie. Spójrz na rezultaty przykładowego testu na listingu 15-2.

Listing 15-2 - Uruchamianie pojedynczego testu jednostkowego z linii poleceń

> symfony test-unit strtolower

1..7
# strtolower()
ok 1 - strtolower() zwraca typ string
ok 2 - strtolower() zamienia duże litery na małe
ok 3 - strtolower() pozostawia małe litery niezmienione
ok 4 - strtolower() pozostawia znaki spoza alfabetu niezmienione
ok 5 - strtolower() pozostawia odstępy niezmienione
ok 6 - strtolower() radzi sobie z literami o różnej wielkości
not ok 7 - strtolower() zamienia puste łańcuchy w napis foo
#     Failed test (.\batch\test.php at line 21)
#            got: ''
#       expected: 'foo'
# Looks like you failed 1 tests of 7.

TIP Słowo kluczowe include na początku listingu 15-1 jest opcjonalne, ale sprawia że plik testu jest niezależnym skryptem PHP, dzięki czemu możesz go wykonać, nie używając polecenia symfony, poprzez wywołanie php test/unit/strtolowerTest.php.

Metody do testowania jednostkowego

Obiekt lime_test zawiera dużą liczbę metod testujących, wymienionych w tabeli 15-2.

Tabela 15-2 - Metody testujące obiektu lime_test

Method | Description ------------------------------------------- | ------------------------------------------------------------- diag($msg) | Wypisuje komunikat, ale nie uruchamia żadnego testu ok($test, $msg) | Sprawdza warunek, test przechodzi jeżeli jest on spełniony is($value1, $value2, $msg) | Porównuje dwie wartości, test przechodzi jeżeli są równe (==) isnt($value1, $value2, $msg) | Porównuje dwie wartości, test przechodzi jeżeli nie są równe like($string, $regexp, $msg) | Sprawdza czy łańcuch pasuje do wzorca - wyrażenia regularnego unlike($string, $regexp, $msg) | Sprawdza czy łańcuch nie pasuje do wzorca - wyrażenia regularnego cmp_ok($value1, $operator, $value2, $msg) | Porównuje dwie wartości z użyciem operatora isa_ok($variable, $type, $msg) | Sprawdza typ argumentu isa_ok($object, $class, $msg) | Sprawdza klasę obiektu can_ok($object, $method, $msg) | Sprawdza dostępność metody w obiekcie lub klasie is_deeply($array1, $array2, $msg) | Sprawdza, czy dwie tablice zawierają te same wartości include_ok($file, $msg) | Sprawdza, czy plik istnieje i jest poprawnie dołączony fail() | Wymusza niepowodzenie testu - przydatne w testowaniu wyjątków pass() | Wymusza powodzenie testu - przydatne w testowaniu wyjątków skip($msg, $nb_tests) | Liczone jako '$nb_tests' testów - przydatne do testów warunkowych todo() | Liczone jako test - przydatne do testów, które mają być napisane

Składnia jest prosta; zauważ, że większość metod pobiera komunikat jako ostatni parametr. Komunikat ten jest wyświetlany, jeżeli test zakończy się sukcesem. Najlepszym sposobem nauczenia się tych metod jest wypróbowanie ich, warto więc spojrzeć na Listing 15-3, w którym wykorzystano je wszystkie.

Listing 15-3 - Testowanie metod obiektu lime_test, w test/unit/exampleTest.php

[php]
<?php

include(dirname(__FILE__).'/../bootstrap/unit.php');

// Namiastki obiektów i funkcji do celów testowych
class myObject
{
  public function myMethod()
  {
  }
}

function throw_an_exception()
{
  throw new Exception('exception thrown');
}

// Inicjalizacja obiektu lime_test
$t = new lime_test(16, new lime_output_color());

$t->diag('hello world');
$t->ok(1 == '1', 'operator porównania ignoruje typ');
$t->is(1, '1', 'string jest konwertowany do typu numerycznego przy porównaniu');
$t->isnt(0, 1, 'zero i jeden nie są równe');
$t->like('test01', '/test\d+/', 'test01 pasuje do wzorca');
$t->unlike('tests01', '/test\d+/', 'tests01 nie pasuje do wzorca');
$t->cmp_ok(1, '<', 2, 'jeden jest mniejsze od dwóch');
$t->cmp_ok(1, '!==', true, 'jeden i true nie są identyczne');
$t->isa_ok('foobar', 'string', '\'foobar\' jest typu string');
$t->isa_ok(new myObject(), 'myObject', 'new tworzy obiekt właściwej klasy');
$t->can_ok(new myObject(), 'myMethod', 'obiekty klasy myObject posiadają metodę myMethod');
$array1 = array(1, 2, array(1 => 'foo', 'a' => '4'));
$t->is_deeply($array1, array(1, 2, array(1 => 'foo', 'a' => '4')),
    'obydwie tablice są takie same');
$t->include_ok('./fooBar.php', 'plik fooBar.php został poprawnie dołączony');

try
{
  throw_an_exception();
  $t->fail('ten kod nie powinien być wykonany po wyrzuceniu wyjątku');
}
catch (Exception $e)
{
  $t->pass('wyjątek udanie przechwycony');
}

if (!isset($foobar))
{
  $t->skip('pominięcie jednego testu w celu utrzymania liczby testów zgodnej z zadeklarowaną', 1);
}
else
{
  $t->ok($foobar, 'foobar');
}

$t->todo('został do stworzenia jeden test');

Znajdziesz sporo innych przykładów wykorzystania tych metod w testach jednostkowych Symfony.

TIP Możesz się zastanawiać, dlaczego lepiej używać is() niż ok(). Komunikat błędu zwracany przez is() jest bardziej szczegółowy; pokazuje obie składowe testu, podczas gdy komunikat ok() informuje jedynie, że warunek został niespełniony.

Parametry testów

Konstruktor klasy lime_test pobiera jako pierwszy parametr liczbę testów, które powinny zostać wykonane. Jeżeli liczba wykonanych testów różni się od tej liczby, lime wyświetli ostrzeżenie. Dla przykładu, zestaw testów z listingu 15-3 daje wyniki przedstawione na listingu 15-4. Podczas inicjalizacji ustalono, że 16 testów będzie uruchomionych, ale wykonano tylko 15, co spowodowało wyświetlenie ostrzeżenia.

Listing 15-4 - Liczba przeprowadzonych testów pomaga w planowaniu testów

[php]
> symfony test-unit example

1..16
# hello world
ok 1 - operator porównania ignoruje typ
ok 2 - string jest konwertowany do typu numerycznego przy porównaniu
ok 3 - zero i jeden nie są równe
ok 4 - test01 pasuje do wzorca
ok 5 - tests01 nie pasuje do wzorca
ok 6 - jeden jest mniejsze od dwóch
ok 7 - jeden i true nie są identyczne
ok 8 - 'foobar' jest typu string
ok 9 - new tworzy obiekt właściwej klasy
ok 10 - obiekty klasy myObject posiadają metodę myMethod
ok 11 - obydwie tablice są takie same
not ok 12 - plik fooBar.php został poprawnie dołączony
#     Failed test (.\test\unit\testTest.php at line 27)
#       Tried to include './fooBar.php'
ok 13 - wyjątek udanie przechwycony
ok 14 # SKIP ominięcie jednego testu w celu utrzymania liczby testów zgodnej z zadeklarowaną
ok 15 # TODO został do stworzenia jeden test
# Looks like you planned 16 tests but only ran 15.
# Looks like you failed 1 tests of 16.

Metoda diag() nie jest liczona jako test. Używaj jej do wyświetlania komentarzy, dzięki czemu wyniki twoich testów będą uporządkowane i czytelne. Z drugiej strony, metody todo() i skip() są liczone jako testy. Kombinacja pass()/fail() wewnątrz bloku try/catch jest liczona jako pojedynczy test.

Dobrze zaplanowana strategia testowania musi zawierać określoną liczbę testów. Jest to bardzo użyteczne przy sprawdzaniu własnych plików testów - zwłaszcza w złożonych przypadkach, kiedy testy są uruchamiane wewnątrz wyrażeń warunkowych lub wyjątków. Jeżeli test zawiedzie w jakimś miejscu, szybko się zorientujesz, ponieważ liczba wykonanych testów będzie różniła się od liczby podanej podczas inicjalizacji.

Drugim parametrem konstruktora jest obiekt reprezentujący wyjście (output), będący instancją klasy pochodnej od lime_output. Najczęściej, kiedy testy są uruchamiane z konsoli, output jest obiektem klasy lime_output_color wykorzystującej kolorowanie tekstu w powłoce, jeżeli jest ono dostępne.

Polecenie test-unit

Polecenie test-unit służy do uruchamiania testów jednostkowych z linii poleceń i oczekuje jako parametrów listy nazw testów lub wzorców plików. Szczegółowo przedstawione jest to na listingu 15-5.

Listing 15-5 - Uruchamianie testów jednostkowych

// Struktura katalogu test
test/
  unit/
    myFunctionTest.php
    mySecondFunctionTest.php
    foo/
      barTest.php

> symfony test-unit myFunction                   ## Uruchom myFunctionTest.php
> symfony test-unit myFunction mySecondFunction  ## Uruchom oba testy
> symfony test-unit 'foo/*'                      ## Uruchom barTest.php
> symfony test-unit '*'                          ## Uruchom wszystkie testy (rekursywnie)

Namiastki, Fixtures i Autoloading

Podczas wykonywania testu jednostkowego funkcja autoloading domyślnie nie jest aktywna. Każda klasa, której używasz podczas testu musi być albo zdefiniowana w pliku testu, albo dołączona własnoręcznie. Dlatego wiele plików testów zaczyna się od grupy linii zawierających wyrażenia include, jak na listingu 15-6.

Listing 15-6 - Dołączanie klas w testach jednostkowych

[php]
<?php

include(dirname(__FILE__).'/../bootstrap/unit.php');
include(dirname(__FILE__).'/../../config/config.php');
require_once($sf_symfony_lib_dir.'/util/sfToolkit.class.php');

$t = new lime_test(7, new lime_output_color());

// isPathAbsolute()
$t->diag('isPathAbsolute()');
$t->is(sfToolkit::isPathAbsolute('/test'), true,
    'isPathAbsolute() zwraca true, jeżeli ścieżka dostępu jest ścieżką bezwzględną');
$t->is(sfToolkit::isPathAbsolute('\\test'), true,
    'isPathAbsolute() zwraca true, jeżeli ścieżka dostępu jest ścieżką bezwzględną');
$t->is(sfToolkit::isPathAbsolute('C:\\test'), true,
    'isPathAbsolute() zwraca true, jeżeli ścieżka dostępu jest ścieżką bezwzględną');
$t->is(sfToolkit::isPathAbsolute('d:/test'), true,
    'isPathAbsolute() zwraca true, jeżeli ścieżka dostępu jest ścieżką bezwzględną');
$t->is(sfToolkit::isPathAbsolute('test'), false,
    'isPathAbsolute() zwraca false, jeżeli ścieżka dostępu jest ścieżką względną');
$t->is(sfToolkit::isPathAbsolute('../test'), false,
    'isPathAbsolute() zwraca false, jeżeli ścieżka dostępu jest ścieżką względną');
$t->is(sfToolkit::isPathAbsolute('..\\test'), false,
    'isPathAbsolute() zwraca false, jeżeli ścieżka dostępu jest ścieżką względną');

W testach jednostkowych musisz utworzyć nie tylko testowany obiekt, ale również obiekt, od którego on zależy. Ponieważ testy jednostkowe muszą być jednolite, zależności od innych klas mogą sprawić że pewien test może zakończyć się niepowodzeniem, jeżeli jedna z tych klas nie działa prawidłowo. W dodatku inicjalizacja prawdziwych obiektów może być kosztowna, zarówno jeżeli chodzi o ilość linii kodu, jak i czas wykonania. Miej na uwadze, że szybkość jest kluczowa w testowaniu jednostkowym, ponieważ powolny proces szybko nuży programistów.

Kiedy zaczniesz dołączać wiele skryptów dla testu jednostkowego, możesz potrzebować prostego mechanizmu automatycznego dołączania plików (autoload). W tym celu klasa sfCore (którą musisz dołączyć własnoręcznie) dostarcza metodę initSimpleAutoload(), która oczekuje ścieżki absolutnej jako parametru. Wszystkie klasy umieszczone w podanej lokalizacji zostaną załadowane. Dla przykładu, jeżeli chesz automatycznie dołączyć wszystkie klasy z katalogu $sf_symfony_lib_dir/util/ umieść na początku swojego skryptu testującego następujące linie:

[php]
require_once($sf_symfony_lib_dir.'/util/sfCore.class.php');
sfCore::initSimpleAutoload($sf_symfony_lib_dir.'/util');

TIP Obiekty wygenerowane przez Propel opierają się na długiej kaskadzie klas, więc jeżeli chcesz testować obiekt Propel, automatyczne dołączanie jest niezbędne. Zauważ również, że Propel do pracy wymaga dołączenia plików z katalogu vendor/propel/ (więc wywołanie metody sfCore ma postać sfCore::initSimpleAutoload(array(SF_ROOT_ DIR.'/lib/model', $sf_symfony_lib_dir.'/vendor/propel'));) oraz dodania jądra Propel do ścieżki include (poprzez wywołanie set_include_path($sf_symfony_lib_dir.'/vendor'.PATH_SEPARATOR.SF_ROOT_DIR.PATH_SEPARATOR.get_include_path()).

Kolejnym dobrym obejściem problemu automatycznego ładowania klas jest użycie obiektów-namiastek. Namiastka jest alternatywną implementacją klasy, gdzie prawdziwe metody są zastąpione metodami, które zwracają ustalone, proste dane. Naśladuje to zachowanie rzeczywistej klasy, ale bez kosztów jej użycia. Dobrym przykładem użycia namiastek jest połączenie z bazą danych lub usługą web service. Na listingu 15-7 testy jednostkowe do mapowania API używają klasy WebService. Zamiast wywoływać prawdziwą metodę fetch() klasy web service, test wykorzystuje namiastkę, która zwraca dane testowe.

Listing 15-7 - Wykorzystanie namiastek w testach jednostkowych

[php]
require_once(dirname(__FILE__).'/../../lib/WebService.class.php');
require_once(dirname(__FILE__).'/../../lib/MapAPI.class.php');

class testWebService extends WebService
{
  public static function fetch()
  {
    return file_get_contents(dirname(__FILE__).'/fixtures/data/fake_web_service.xml');
  }
}

$myMap = new MapAPI();

$t = new lime_test(1, new lime_output_color());

$t->is($myMap->getMapSize(testWebService::fetch(), 100));

Dane testowe mogą być bardziej skomplikowane niż łańcuch, czy wywołanie metody. Złożone dane testowe są często określane jako fixtures. Dla czytelności kodu często lepiej jest trzymać fixtures w osobnych plikach, zwłaszcza jeżeli są używane przez więcej niż jeden plik testów. Ponadto nie zapominaj, że Symfony może w łatwy sposób wczytać plik YAML do tablicy za pomocą metody sfYAML::load(). Oznacza to, że zamiast tworzyć długie tablice w PHP, możesz zapisać własne dane testowe w pliku YAML, jak na listingu 15-8.

Listing 15-8 - Wykorzystanie plików fixture w testach jednostkowych

[php]
// W pliku fixtures.yml:
-
  input:   '/test'
  output:  true
  comment: isPathAbsolute() zwraca true, jeżeli ścieżka dostępu jest ścieżką bezwzględną
-
  input:   '\\test'
  output:  true
  comment: isPathAbsolute() zwraca true, jeżeli ścieżka dostępu jest ścieżką bezwzględną
-
  input:   'C:\\test'
  output:  true
  comment: isPathAbsolute() zwraca true, jeżeli ścieżka dostępu jest ścieżką bezwzględną
-
  input:   'd:/test'
  output:  true
  comment: isPathAbsolute() zwraca true, jeżeli ścieżka dostępu jest ścieżką bezwzględną
-
  input:   'test'
  output:  false
  comment: isPathAbsolute() zwraca false, jeżeli ścieżka dostępu jest ścieżką względną
-
  input:   '../test'
  output:  false
  comment: isPathAbsolute() zwraca false, jeżeli ścieżka dostępu jest ścieżką względną
-
  input:   '..\\test'
  output:  false
  comment: isPathAbsolute() zwraca false, jeżeli ścieżka dostępu jest ścieżką względną

// W pliku testTest.php
<?php

include(dirname(__FILE__).'/../bootstrap/unit.php');
include(dirname(__FILE__).'/../../config/config.php');
require_once($sf_symfony_lib_dir.'/util/sfToolkit.class.php');
require_once($sf_symfony_lib_dir.'/util/sfYaml.class.php');

$testCases = sfYaml::load(dirname(__FILE__).'/fixtures.yml');

$t = new lime_test(count($testCases), new lime_output_color());

// isPathAbsolute()
$t->diag('isPathAbsolute()');
foreach ($testCases as $case)
{
  $t->is(sfToolkit::isPathAbsolute($case['input']), $case['output'],$case['comment']);
}

Testy funkcjonalne

Testy funkcjonalne walidują elementy składowe twoich aplikacji. Symulują sesję użytkownika, wysyłają żądania i sprawdzają elementy obecne w odpowiedzi tak, jak Ty robiłbyś to ręcznie, aby upewnić się, czy akcja robi to, co powinna. W testach funkcjonalnych badasz scenariusz odpowiadający danemu przypadkowi użycia.

///

Jak wyglądają testy funkcjonalne?

Mógłbyś przeprowadzać swoje testy funkcjonalne używając przeglądarki tekstowej i wielu asercji wykorzystujących wyrażenia regularne, ale byłaby to duża strata czasu. Symfony dostarcza specjalnego obiektu, zwanego sfBrowser, który symuluje działanie przeglądarki podłączonej do aplikacji Symfony bez konieczności korzystania z serwera - i wolnej od opóźnień związanych z transmisją przez HTTP. Obiekt ten daje dostęp do istotnych obiektów każdego żądania (request, sesji, context, response). Symfony dostarcza również klasę będąca jej rozszerzeniem, sfTestBrowser, zaprojektowaną specjalnie do testów funkcjonalnych i posiadającą, oprócz wszystkich możliwości klasy sfBrowser, kilka eleganckich metod służacych do sprawdzania asercji.

Testy funkcjonalne zaczynają się tradycyjnie od inicjalizacji obiektu test browser. Obiekt ten wysyła żądania do akcji i weryfikuje, czy pewne elementy są obecne w odpowiedzi.

Przykładowo, za każdym razem, gdy generujesz szkielet modułu za pomocą polecenia init-module lub propel-init-crud, Symfony tworzy prosty test funkcjonalny dla tego modułu. Test ten wykonuje żądanie do domyślnej akcji modułu, po czym sprawdza kod statusu odpowiedzi, moduł i akcję wyznaczoną przez system routingu oraz obecność pewnej frazy w treści odpowiedzi. Dla modułu foobar wygenerowany plik foobarActionsTest.php wygląda jak na listingu 15-9.

Listing 15-9 - Domyślny test funkcjonalny dla nowego modułu w pliku tests/functional/frontend/foobarActionsTest.php

[php]
<?php

include(dirname(__FILE__).'/../../bootstrap/functional.php');

// Create a new test browser
$browser = new sfTestBrowser();
$browser->initialize();

$browser->
  get('/foobar/index')->
  isStatusCode(200)->
  isRequestParameter('module', 'foobar')->
  isRequestParameter('action', 'index')->
  checkResponseElement('body', '!/This is a temporary page/')
;

TIP Metody obiektu browser zwracają obiekt sfTestBrowser, więc możesz łańcuchowo wywoływać metody w celu zwiększenia czytelności twoich testów. Nazywa się to płynnym interfejsem obiektu, ponieważ nic nie przerywa strumienia wywołań metod.

Testy funkcjonalne mogą zawierać wiele żądań i bardziej skomplikowane asercje; wkrótce, w kolejnych podrozdziałach, odkryjesz wszystkie możliwości.

Aby uruchomić test funkcjonalny użyj polecenia test-functional z linii poleceń Symfony, jak pokazano na listingu 15-10. Oczekuje ono nazwy aplikacji i nazwy testu (bez przyrostka Test.php).

Listing 15-10 - Uruchamianie pojedynczego testu funkcjonalnego z linii poleceń

> symfony test-functional frontend foobarActions

# get /comment/index
ok 1 - status code is 200
ok 2 - request parameter module is foobar
ok 3 - request parameter action is index
not ok 4 - response selector body does not match regex /This is a temporary page/
# Looks like you failed 1 tests of 4.
1..4

Wygenerowane testy funkcjonalne dla nowego modułu domyślnie kończą się niepowodzeniem. Dzieje sie tak dlatego, że w nowo utworzonym module akcja index przekierowuje do strony z gratulacjami (zamieszczonej w module default Symfony), która zawiera frazę "This is a temporary page" ("To jest strona tymczasowa" - tłum.). Dopóki nie zmodyfikujesz akcji index, testy dla tego modułu będą kończyły się porażką, co gwarantuje że aplikacja z niedokończonym modułem nie może przejść testów pomyślnie.

NOTE W testach funkcjonalnych funkcja autoloading (automatyczne ładowanie klas) jest włączona, więc nie musisz dołączać plików własnoręcznie.

Korzystanie z obiektu sfTestBrowser

Przeglądarka testowa jest w stanie wykonywać żądania GET i POST. W obu przypadkach wykorzystuje rzeczywisty URI jako parametr. Listing 15-11 przedstawia, jak korzystać z obiektu sfTestBrowser, aby symulować żądania.

Listing 15-11 - Symulowanie żądań za pomocą obiektu sfTestBrowser

[php]
include(dirname(__FILE__).'/../../bootstrap/functional.php');

// Utworzenie nowej przeglądarki testowej
$b = new sfTestBrowser();
$b->initialize();

$b->get('/foobar/show/id/1');                   // GET request
$b->post('/foobar/show', array('id' => 1));     // POST request

// Metody get() i post() są skrótami do metody call()
$b->call('/foobar/show/id/1', 'get');
$b->call('/foobar/show', 'post', array('id' => 1));

// Metoda call() może symulować żądania za pomocą każdej metody
$b->call('/foobar/show/id/1', 'head');
$b->call('/foobar/add/id/1', 'put');
$b->call('/foobar/delete/id/1', 'delete');

Typowa sesja przeglądarki to nie tylko żądania określonych akcji, ale również kliknięcia na linkach oraz na przyciskach przeglądarki. Jak pokazano na listingu 15-12, obiekt sfTestBrowser jest w stanie je zasymulować.

Listing 15-12 - Symulowanie nawigacji za pomocą sfTestBrowser

[php]
$b->get('/');                  // Wczytanie strony głównej (żądanie)
$b->get('/foobar/show/id/1');
$b->back();                    // Cofnij o jedną stronę w historii
$b->forward();                 // Do przodu o jedną stronę w historii
$b->reload();                  // Wczytaj ponownie bieżącą stronę
$b->click('go');               // Znajdź link lub przycisk 'go' i kliknij go

Przeglądarka testowa radzi sobie ze stosem wywołań, więc metody back() i forward() działają tak, jak w rzeczywistej przeglądarce.

TIP Przeglądarka testowa posiada własne mechanizmy do zarządzania sesjami (sfTestStorage) i ciasteczkami.

Wśród interakcji wymagających najczęstszego testowania, najprawdopodobniej na pierwszym miejscu znajdują się te związane z formularzami. Aby zasymulować wypełnienie formularza i jego zatwierdzenie, masz do wyboru trzy opcje. Możesz albo wykonać żądanie POST zawierające parametry, które chcesz wysłać, albo wywołać metodę click() z tablicą zawierającą parametry formularza, albo wypełnić pola jedno za drugim i kliknąć w przycisk submit. Tak czy inaczej, wszystkie te metody kończą się takim samym żądaniem POST. Listing 15-13 zawiera przykład.

Listing 15-13 - Symulowanie wypełnienia formularza za pomocą obiektu sfTestBrowser

[php]
// Przykładowy szablon w modules/foobar/templates/editSuccess.php
<?php echo form_tag('foobar/update') ?>
  <?php echo input_hidden_tag('id', $sf_params->get('id')) ?>
  <?php echo input_tag('name', 'foo') ?>
  <?php echo submit_tag('go') ?>
  <?php echo textarea('text1', 'foo') ?>
  <?php echo textarea('text2', 'bar') ?>
</form>

// Przykładowy test funkcjonalny dla tego formularza
$b = new sfTestBrowser();
$b->initialize();
$b->get('/foobar/edit/id/1');

// Opcja 1: żądanie POST
$b->post('/foobar/update', array('id' => 1, 'name' => 'dummy', 'commit' => 'go'));

// Opcja 2: Wciśnij przycisk submit z danymi parametrami
$b->click('go', array('name' => 'dummy'));

// Opcja 3: Wypełnij pola formularza jedno za drugim, po czym wciśnij przycisk submit
$b->setField('name', 'dummy')->
    click('go');

NOTE W przypadku drugiej i trzeciej opcji domyślne wartości formularza są automatycznie dołączane do wysyłanych z formularza danych, poza tym adres docelowy formularza nie musi być określony.

Kiedy akcja kończy się przekierowaniem redirect(), przeglądarka testowa nie podąża za nim automatycznie; musisz zrobić to samemu używając metody followRedirect(), jak zademonstrowano to na listingu 15-14.

Listing 15-14 - Przeglądarka testowa nie podąża automatycznie za przekierowaniami redirect()

[php]
// Przykładowa akcja w modules/foobar/actions/actions.class.php
public function executeUpdate()
{
  ...
  $this->redirect('foobar/show?id='.$this->getRequestParameter('id'));
}

// Przykładowy test funkcjonalny tej akcji
$b = new sfTestBrowser();   
$b->initialize();
$b->get('/foobar/edit?id=1')->
    click('go', array('name' => 'dummy'))->
    isRedirected()->   // Sprawdź czy żądanie zostało przekierowane
    followRedirect();    // Podąż za przekierowaniem

Jest jeszcza jedna metoda o której powinieneś wiedzieć: restart() powoduje reinicjalizację historii przeglądarki, sesji i ciasteczek - tak jakbyś zrestartował swoją przeglądarkę.

Od momentu wysłania pierwszego żądania obiekt sfTestBrowser może dać dostęp do obiektów request, context i response. Oznacza to, że możesz sprawdzić wiele rzeczy, począwszy od zawartości strony poprzez nagłówki odpowiedzi, parametry żądania i konfiguracji:

[php]
$request  = $b->getRequest();
$context  = $b->getContext();
$response = $b->getResponse();

SIDEBAR Obiekt sfBrowser

Wszystkie metody opisane na listingach od 15-10 do 15-13 można stosować, za pomocą obiektu sfBrowser, nie tylko do celów testowych. Można je wywoływać jak poniżej:

[php] // Utwórz przeglądarkę $b = new sfBrowser(); $b->initialize(); $b->get('/foobar/show/id/1')-> setField('name', 'dummy')-> click('go'); $content = $b()->getResponse()->getContent(); ...

Obiekt sfBrowser jest bardzo użytecznym narzędziem w skryptach wsadowych (batch scripts), na przykład w sytuacji gdybyś chciał pobrać listę stron, aby wygenerować cache'owaną wersję dla każdej (zajrzyj do rozdziału 18 po bardziej szczegółowy przykład).

Używanie asercji

Zważywszy że obiekt sfTestBrowser posiada dostęp do odpowiedzi (response) oraz innych komponentów żądania, możesz te komponenty testować. Mógłbyś utworzyć nowy obiekt lime_test w tym celu, ale na szczęście obiekt sfTestBrowser posiada metodę test(), która zwraca obiekt lime_test, którego metod, opisanych wcześniej, możesz użyć. Sprawdź listing 15-15, aby dowiedzieć się jak wykonywać asercje za pomocą sfTestBrowser.

Listing 15-15 - Możliwości testowe przeglądarki testowej udostępniane za pomocą metody test()

[php]
$b = new sfTestBrowser();
$b->initialize();
$b->get('/foobar/edit/id/1');
$request  = $b->getRequest();
$context  = $b->getContext();
$response = $b->getResponse();

// Dostęp do metod lime_test poprzez metodę test()
$b->test()->is($request->getParameter('id'), 1);
$b->test()->is($response->getStatuscode(), 200);
$b->test()->is($response->getHttpHeader('content-type'), 'text/html;charset=utf-8');
$b->test()->like($response->getContent(), '/edit/');

NOTE Metody getResponse(), getContext(), getRequest() i test() nie zwracają obiektu sfTestBrowser, dlatego nie możesz użyć łańcucha wywołań metod sfTestBrowser za nimi.

Możesz w prosty sposób sprawdzić przychodzące i wychodzące ciasteczka korzystając z obiektów request i response, jak pokazano na listingu 15-16.

Listing 15-16 - Testowanie ciasteczek za pomocą sfTestBrowser

[php]
$b->test()->is($request->getCookie('foo'), 'bar');     // Przychodzące ciasteczko
$cookies = $response->getCookies();
$b->test()->is($cookies['foo'], 'foo=bar');            // Wysyłane ciasteczko

Używanie metody test() do sprawdzania elementów żądania skutkuje długimi liniami kodu. Na szczęście sfTestBrowser zawiera grupę metod pośredniczących, które pomogą Ci utrzymać zwięzłość i czytelność testów funkcjonalnych, ponieważ zwracają obiekt sfTestBrowser. Możesz dla przykładu napisać kod z listingu 15-15 w szybszy sposób, jak na listingu 15-17.

Listing 15-17 - Testowanie z bezpośrednim wykorzystaniem sfTestBrowser

[php]
$b = new sfTestBrowser();
$b->initialize();
$b->get('/foobar/edit/id/1')->
    isRequestParameter('id', 1)->
    isStatusCode()->
    isResponseHeader('content-type', 'text/html; charset=utf-8')->
    responseContains('edit');

Kod 200 jest domyślną wartością parametru oczekiwanego przez metodę isStatusCode(), więc możesz ją wywoływać bez argumentu aby sprawdzić poprawność odpowiedzi.

Kolejną zaletą korzystania z metod pośredniczących jest fakt, że nie musisz podawać tekstu wyświetlanego przez metody lime_test. Komunikaty są generowane automatycznie przez metody pośredniczące, dzięki czemu wyniki testów są jasne i czytelne.

[php]
# get /foobar/edit/id/1
ok 1 - request parameter "id" is "1"
ok 2 - status code is "200"
ok 3 - response header "content-type" is "text/html"
ok 4 - response contains "edit"
1..4

W praktyce, metody pośredniczące z listingu 15-17 wystarczają do większości testów, więc raczej rzadko będziesz korzystał z metody test() obiektu sfTestBrowser.

Listing 15-14 pokazał, że sfTestBrowser nie podąża automatycznie za przekierowaniami. Ma to jedną zaletę: możesz testować przekierowanie. Dla przykładu, listing 15-18 pokazuje jak testować odpowiedź z listingu 15-14.

Listing 15-18 - Testowanie przekierowań za pomocą sfTestBrowser

[php]
$b = new sfTestBrowser();
$b->initialize();
$b->
    get('/foobar/edit/id/1')->
    click('go', array('name' => 'dummy'))->
    isStatusCode(200)->
    isRequestParameter('module', 'foobar')->
    isRequestParameter('action', 'update')->

    isRedirected()->      // Sprawdź, czy odpowiedź jest przekierowaniem
    followRedirect()->    // Podąż za przekierowaniem

    isStatusCode(200)->
    isRequestParameter('module', 'foobar')->
    isRequestParameter('action', 'show');

Używanie selektorów CSS

Wiele testów funkcjonalnych waliduje poprawność strony poprzez sprawdzenie obecności pewnego tekstu w jej zawartości. Z pomocą wyrażeń regularnych w metodzie responseContains() możesz sprawdzić wyświetlany tekst, atrybuty znaczników lub wartości. Jeżeli jednak zechcesz sprawdzić coś zagrzebane głęboko w DOM odpowiedzi, wyrażenia regularne mogą okazać się niedoskonałe.

Dlatego obiekt sfTestBrowser zawiera metodę getResponseDom(). Zwraca ona obiekt DOM libXML2, łatwiejszy do przeparsowania i testowania niż czysty tekst. Przykład jej użycia zamieszczono na listingu 15-19.

Listing 15-19 - Przeglądarka testowa daje dostęp do zawartości odpowiedzi w postaci obiektu DOM

[php]
$b = new sfTestBrowser();
$b->initialize();
$b->get('/foobar/edit/id/1');
$dom = $b->getResponseDom();
$b->test()->is($dom->getElementsByTagName('input')->item(1)->getAttribute('type'),'text');

Ale parsowanie dokumentu HTML za pomocą metod PHP DOM nadal nie jest wystarczająco szybkie i łatwe. Jeżeli jesteś zaznajomiony z selektorami CSS, wiesz że są one o wiele skuteczniejsze w wydobywaniu elementów z dokumentu HTML. Symfony dostarcza narzędzie zwane sfDomCssSelector, które oczekuje dokumentu DOM jak parametru konstruktora. Posiada metodę getTexts(), która zwraca tablicę łańcuchów stosownie do selektora CSS oraz metodę getElements(), która zwraca tablicę elementów DOM. Zobacz przykład na listingu 15-20.

Listing 15-20 - Przeglądarka testowa daje dostęp od zawartości odpowiedzi za pomocą obiektu sfDomCssSelector

[php]
$b = new sfTestBrowser();
$b->initialize();
$b->get('/foobar/edit/id/1');
$c = new sfDomCssSelector($b->getResponseDom());
$b->test()->is($c->getTexts('form input[type="hidden"][value="1"]'), array(''));
$b->test()->is($c->getTexts('form textarea[name="text1"]'), array('foo'));
$b->test()->is($c->getTexts('form input[type="submit"]'), array(''));

W tym ciągłym pościgu za zwięzłością i przejrzystością Symfony dostarcza skrót: metodę pośredniczącą checkResponseElement(). Kod z listingu 15-20 zapisany z jej pomocą znajduje się na listingu 15-21.

Listing 15-21 - Przeglądarka testowa daje dostęp do elementów odpowiedzi za pomocą selektorów CSS

[php]
$b = new sfTestBrowser();
$b->initialize();
$b->get('/foobar/edit/id/1')->
    checkResponseElement('form input[type="hidden"][value="1"]', true)->
    checkResponseElement('form textarea[name="text1"]', 'foo')->
    checkResponseElement('form input[type="submit"]', 1);

Zachowanie metody checkResponseElement() zależy od typu drugiego argumentu przez nią pobieranego:

  • Jeżeli jest to wartość logiczna (boolean), sprawdza czy element pasujący do selektora CSS istnieje.
  • Jeżeli jest to liczba całkowita (integer), sprawdza czy selektor CSS zwrócił podaną liczbę rezultatów.
  • Jeżeli jest to wyrażenie regularne, sprawdza czy pierwszy element znaleziony przez selektor CSS pasuje do niego.
  • Jeżeli jest to wyrażenie regularne poprzedzone przez !, sprawdza czy pierwszy element nie pasuje do wzorca.
  • W innych przypadkach porównuje pierwszy element znaleziony przez selektor CSS z drugim argumentem jako tekst.

Metoda akceptuje trzeci, opcjonalny, parametr w postaci tablicy asocjacyjnej. Pozwala to na testowanie nie tylko pierwszego elementu zwróconego przez selektor (jeżeli zwraca kilka), ale również innego elementu na określonej pozycji, jak pokazano na listingu 15-22.

Listing 15-22 - Użycie opcji position do dopasowania elementu na określonej pozycji

[php]
$b = new sfTestBrowser();
$b->initialize();
$b->get('/foobar/edit?id=1')->
    checkResponseElement('form textarea', 'foo')->
    checkResponseElement('form textarea', 'bar', array('position' => 1));

Tablica options może być również użyta do wykonania dwóch testów w tym samym czasie. Możesz sprawdzić, czy istnieje element pasujący do selektora oraz jak wiele ich jest, jak zademonstrowano na listingu 15-23.

Listing 15-23 - Użycie opcji count do sprawdzenia liczby dopasowań

[php]
$b = new sfTestBrowser();
$b->initialize();
$b->get('/foobar/edit?id=1')->
    checkResponseElement('form input', true, array('count' => 3));

Narzędzie selector posiada ogromne możliwości. Akceptuje większość z selektorów CSS 2.1 i możesz użyć go w złożonych zapytaniach jak te na listingu 15-24.

Listing 15-24 - Przykład złożonych selektorów CSS akceptowanych przez checkResponseElement()

[php]
$b->checkResponseElement('ul#list li a[href]', 'click me');
$b->checkResponseElement('ul > li', 'click me');
$b->checkResponseElement('ul + li', 'click me');
$b->checkResponseElement('h1, h2', 'click me');
$b->checkResponseElement('a[class$="foo"][href*="bar.html"]', 'my link');

Praca w środowisku testowym

Obiekt sfTestBrowser używa specjalnego front kontrolera przeznaczonego dla środowiska testowego. Domyślna konfiguracja dla tego środowiska przedstawiona jest na listingu 15-25.

Listing 15-25 - Domyślna konfiguracja środowiska testowego w myapp/config/settings.yml

[php]
test:
  .settings:
    # E_ALL | E_STRICT & ~E_NOTICE = 2047
    error_reporting:        2047
    cache:                  off
    web_debug:              off
    no_script_name:         off
    etag:                   off

Cache i pasek debugowania (web_debug) są wyłączone w tym środowisku. Jednakże, w trakcie wykonywania kodu są zapisywane logi w pliku logu innym niż pliki logów środowisk dev i prod, więc możesz je sprawdzać niezależnie (myproject/log/myapp_test.log). W tym środowisku wyjątki nie zatrzymują wykonywania skryptów - więc możesz uruchomić cały zestaw testów nawet, jeżeli jeden nie działa. Możesz określić specyficzne ustawienia połączenia z bazą danych, na przykład w celu użycia bazy danych zawierającej danymi testowymi.

Zanim użyjesz obiektu sfTestBrowser, musisz go zainicjalizować. Jeżeli potrzebujesz, możesz określić nazwę hosta dla aplikacji i adres IP dla klienta - w przypadku, gdy twoja aplikacja wykorzystuje te dwa parametry. Listing 15-26 przedstawia jak to zrobić.

Listing 15-26 - Tworzenie przegladarki testowej z nazwą hosta i adresem IP

[php]
$b = new sfTestBrowser();
$b->initialize('myapp.example.com', '123.456.789.123');

Polecenie test-functional

Polecenie test-functional może uruchomić jeden lub więcej testów funkcjonalnych, w zależności od liczby podanych argumentów. Zasady są takie same jak w przypadku polecenia test-unit, z wyjątkiem tego, że polecenie test-functional zawsze oczekuje nazwy aplikacji jako pierwszego argumentu, jak pokazano na listingu 15-27.

Listing 15-27 - Składnia polecenia test-functional

[php]
// Struktura katalogu test
test/
  functional/
    frontend/
      myModuleActionsTest.php
      myScenarioTest.php
    backend/
      myOtherScenarioTest.php

## Uruchamia wszystkie testy funkcjonalne dla jednej aplikacji (rekursywnie)
> symfony test-functional frontend

## Uruchamia jeden wybrany test
> symfony test-functional frontend myScenario

## Uruchamia wiele testów o nazwie pasującej do wzoraca
> symfony test-functional frontend my*

Standardy nazewnictwa testów

Sekcja ta prezentuje kilka dobrych nawyków mających na celu utrzymanie twoich testów uporządkowanymi i łatwymi do zarządzania. Wskazówki te dotyczą organizacji plików testów jednostkowych i funkcjonalnych.

Jeżeli chodzi o strukturę plików, powinieneś nazywać testy jednostkowe używając nazwy testowanej klasy, zaś testy funkcjonalne używając nazwy testowanego modułu lub scenariusza. Zobacz przykład na listingu 15-28. Twój katalog 'test/' będzie wkrótce zawierał sporo plików i na dłuższą metę znalezienie danego testu może okazać się zadaniem trudnym, jeżeli nie będziesz postępował według podanych wytycznych.

Listing 15-28 - Przykład stosowania standardu nazewnictwa plików

test/
  unit/
    myFunctionTest.php
    mySecondFunctionTest.php
    foo/
      barTest.php
  functional/
    frontend/
      myModuleActionsTest.php
      myScenarioTest.php
    backend/
      myOtherScenarioTest.php

Dobrą praktyką w testach jednostkowych jest grupowanie testów według funkcji lub metody i poprzedzanie każdej takiej grupy wywołaniem metody 'diag()'. Komunikaty każdego testu jednostkowego powinny zawierać nazwę testowanej funkcji lub metody, po której następuje czasownik i właściwość, więc wyniki testów wyglądają jakby opisywały właściwość obiektu. Listing 15-29 zawiera przykład.

Listing 15-29 - Przykład stosowania standardu nazewnictwa testów jednostkowych [php] // srttolower() $t->diag('strtolower()'); $t->isa_ok(strtolower('Foo'), 'string', 'strtolower() zwraca typ string'); $t->is(strtolower('FOO'), 'foo', 'strtolower() zamienia duże litery na małe');

# strtolower()
ok 1 - strtolower() zwraca typ string
ok 2 - strtolower() zamienia duże litery na małe

Testy funkcjonalne powinny być zgrupowane według stron i zaczynać się od żądania. Listing 15-30 przedstawia ten schemat postępowania.

Listing 15-30 - Przykład stosowania standardu nazewnictwa testów funkcjonalnych

[php]
$browser->
  get('/foobar/index')->
  isStatusCode(200)->
  isRequestParameter('module', 'foobar')->
  isRequestParameter('action', 'index')->
  checkResponseElement('body', '/foobar/')
;

# get /comment/index
ok 1 - status code is 200
ok 2 - request parameter module is foobar
ok 3 - request parameter action is index
ok 4 - response selector body matches regex /foobar/

Jeżeli będziesz postępował zgodnie z tymi zasadami, wyniki twoich testów będą wystarczająco przejrzyste, aby używać ich jako dokumentacji technicznej w twoim projekcie - w niektórych przypadkach na tyle, że faktyczna dokumentacja techniczna będzie zbędna.

Szczególne potrzeby testowe

Narzędzia do testów jednostkowych i funkcjonalnych dostarczane przez Symfony powinny wystarczyć w większości przypadków. W rozdziale tym wymieniono kilka dodatkowych technik, które mogą pomóc w rozwiązywaniu częstych problemów występujących przy automatycznym testowaniu: uruchamianie testów w izolowanym środowisku, dostęp do bazy danych wewnątrz testów, testowanie cache oraz testowanie interakcji po stronie klienta.

Wykonywanie testów w frameworku do uruchamiania zautomatyzowanych testów

Polecenia 'test-unit' oraz 'test-functional' mogą uruchamiać pojedynczy test lub zbiór testów. Jeżeli jednak użyjesz je bez podania parametrów, uruchomią wszystkie testy jednostkowe i funkcjonalne zapisane w katalogu 'test/'. Wykorzystywany jest specjalny mechanizm w celu odizolowania każdego pliku testu w niezależnej "piaskownicy", aby zapobiec ryzyku wzajemnego wpływu testów na siebie. Ponadto, skoro nie ma sensu stosować takiego samego wyjścia jak w przypadku pojedynczego pliku testu (rezultaty mogą zawierać kilka tysięcy linii), wyniki testów są zwarte w synthetic widoku. Dlatego do wykonania dużej liczby plików testowych wykorzystuje się framework (test harness) do uruchamiania zautomatyzowanych testów posiadający specjalne możliwości. Opiera się on na komponencie frameworka lime zwanym 'lime harness'. Pokazuje on status wykonania testów dla każdego pliku oraz końcowe podsumowanie zawierające liczbę testów zakończonych sukcesem w stosunku do liczby wszystkich wykonanych testów, co widać na listingu 15-31.

Listing 15-31 - Uruchamianie wszystkich testów w frameworku do zautomatyzowanych testów

> symfony test-unit

unit/myFunctionTest.php................ok
unit/mySecondFunctionTest.php..........ok
unit/foo/barTest.php...................not ok

Failed Test                     Stat  Total   Fail  List of Failed
------------------------------------------------------------------
unit/foo/barTest.php               0      2      2  62 63
Failed 1/3 test scripts, 66.66% okay. 2/53 subtests failed, 96.22% okay.

Testy wykonywane są taki sam sposób, jak wtedy gdy uruchamiasz je pojedynczo, tylko wyświetlane wyniki są krótsze, przez co bardziej użyteczne. W szczególności, końcowy wykres koncentruje się na liczbie testów zakończonych niepowodzeniem oraz pomaga w ich lokalizacji.

Możesz uruchomić wszystkie testy jednocześnie używając polecenia 'test-all', które również wykorzystuje framework do przeprowadzania zautomatyzowanych testów, jak pokazano na listingu 15-32. Jest to rzecz, którą powinieneś robić zawsze przed przeniesiem aplikacji do środowiska produkcyjnego, aby upewnić się że nie nastąpiła żadna regresja od czasu poprzedniego wydania.

Listing 15-32 - Uruchamianie wszystkich testów projektu

> symfony test-all

Dostęp do bazy danych

Testy jednostkowe często wymagają dostępu do bazy danych. Połączenie z bazą danych jest inicjowane automatycznie przy pierwszym wywołaniu metody 'sfTestBrowser::get()'. Jeżeli jednak chcesz uzyskać dostęp do bazy danych przed użyciem 'sfTestBrowser', musisz samemu zainicjować obiekt 'sfDatabaseManager', jak na listingu 15-33.

Listing 15-33 - Inicjowanie bazy danych w teście

[php]
$databaseManager = new sfDatabaseManager();
$databaseManager->initialize();

// Możesz, opcjonalnie, uzyskać bieżące połączenie z bazą danych
$con = Propel::getConnection();

Zanim zaczniesz testy, powinieneś wypełnić bazę danych danymi testowymi. Można do tego użyć obiektu 'sfPropelData'. Obiekt ten może wczytać dane z pliku, tak jak polecenie 'propel-load-data', lub z tablicy, jak pokazano na listingu 15-34.

Listing 15-34 - Wypełnienie bazy danych danymi testowymi

[php]
$data = new sfPropelData();

// Wczytanie danych z pliku
$data->loadData(sfConfig::get('sf_data_dir').'/fixtures/test_data.yml');

// Wczytanie danych z tablicy
$fixtures = array(
  'Article' => array(
    'article_1' => array(
      'title'      => 'foo title',
      'body'       => 'bar body',
      'created_at' => time(),
    ),
    'article_2'    => array(
      'title'      => 'foo foo title',
      'body'       => 'bar bar body',
      'created_at' => time(),
    ),
  ),
);
$data->loadDataFromArray($fixtures);

Następnie możesz używać obiektów Propela, stosownie do swoich potrzeb, jak w normalnej aplikacji. Pamiętaj o dołączeniu (include) ich plików w testach jednostkowych (możesz użyć metody sfAutoload::sfSimpleAutoloading() w celu automatyzacji, jak opisano to wcześniej w tym rozdziale w sekcji "Stubs, Fixtures, and Autoloading"). Obiekty Propel są automatycznie ładowane w testach funkcjonalnych.

Testowanie cache

Kiedy uruchamiasz mechanizm cache w aplikacji, testy funkcjonalne powinny zweryfikować czy cache'owane akcje działają jak powinny.

Pierwszą rzeczą, jaką należy zrobić, jest uaktywnienie cache w środowisku testowym (w pliku 'settings.yml'). Następnie, jeżeli chesz sprawdzać, kiedy strona jest generowana, a kiedy wczytywana z cache, powinineneś użyć metody 'isCached()' obiektu 'sfTestBrowser'. Listing 15-35 przedstawia tą metodę.

Listing 15-35 - Testowanie cache za pomoca metody 'isCached()'

[php]
<?php

include(dirname(__FILE__).'/../../bootstrap/functional.php');

// Create a new test browser
$b = new sfTestBrowser();
$b->initialize();

$b->get('/mymodule');
$b->isCached(true);       // Sprawdza, czy odpowiedź pochodzi z cache
$b->isCached(true, true); // Sprawdza, czy odpowiedź pochodzi z cache i posiada layout
$b->isCached(false);      // Sprawdza, czy odpowiedź nie pochodzi z cache

NOTE Nie musisz czyścić cache na początku testu funkcjonalnego, robi to za Ciebie skrypt inicjujący.

Testowanie interakcji po stronie klienta

Główną wadą przedstawionych poprzednio technik jest fakt, że nie potrafią symulować działania JavaScript. Przy bardzo skomplikowanych interakcjach, jak na przykład interakcje AJAX, musisz mieć możliwość dokładnego odtworzenia sygnałów wejściowych z myszki i klawiatury użytkownika i wykonywania skryptów po stronie klienta. Zwykle, testy te są wykonywane ręcznie, ale jest to bardzo czasochłonne i podatne na błędy.

Rozwiązaniem jest Selenium (http://www.openqa.org/selenium/), które jest frameworkiem testowym napisanym w całości w JavaScript. Wykonuje on zbiór akcji na danej stronie tak, jak robiłby to użytkownik korzystający z okna przeglądarki. Przewaga nad obiektem 'sfBrowser' polega na tym, że Selenium jest w stanie wykonywać JavaScript, więc możesz przy jego pomocy testować nawet interakcje AJAX.

Selenium nie jest domyślnie dołączone do Symfony. W celu instalacji musisz utworzyć katalog 'selenium/' w katalogu 'web/' i rozpakować do niego zawartość pliku (http://www.openqa.org/selenium-core/download.action). Jest tak dlatego, ponieważ Selenium opiera się na JavaScript, a standardy bezpieczeństwa w większości przeglądarek nie pozwoliłyby uruchomić Selenium, dopóki nie znajdowałby się on na tym samym hoście i porcie co twoja aplikacja.

CAUTION Pamiętaj, aby nie przenosić katalogu 'selenium/' na serwer produkcyjny, gdzie mógłby być dostępny dla każdego posiadającego dostęp przez przeglądarkę do katalogu głównego twojej aplikacji.

Testy Selenium są pisane w HTML-u i trzymane w katalogu 'web/selenium/tests/'. Listing 15-36 pokazuje przykład, zawierający test funkcjonalny, w którym ładowana jest strona startowa, klikany jest link "kliknij mnie" i oczekiwany jest tekst "Witaj, świecie!" w odpowiedzi. Pamiętaj, że aby uzyskać dostęp do aplikacji w środowisku testowym, musisz określić 'myapp_test.php' jako front controller.

*Listing 15-36 - Przykład testu Selenium w web/selenium/test/testIndex.html*

[php]
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
  <meta content="text/html; charset=UTF-8" http-equiv="content-type">
  <title>Index tests</title>
</head>
<body>
<table cellspacing="0">
<tbody>
  <tr><td colspan="3">Pierwszy krok</td></tr>
  <tr><td>open</td>              <td>/myapp_test.php/</td> <td>&nbsp;</td></tr>
  <tr><td>clickAndWait</td>      <td>link=kliknij mnie</td><td>&nbsp;</td></tr>
  <tr><td>assertTextPresent</td> <td>Witaj, świecie!</td>  <td>&nbsp;</td></tr>
</tbody>
</table>
</body>
</html>

Przypadek testowy jest reprezentowany przez dokument HTML zawierający tabelę z trzema kolumnami: polecenie, element docelowy, wartość. Nie wszystkie polecenia pobierają wartość. W takim przypadku, możesz albo zostawić pustą kolumnę, albo użyć ' ', aby tablica wyglądała lepiej. Kompletna lista poleceń jest zamieszczona na stronie Selenium.

Musisz także dodać test do globalnego zestawu testów poprzez dodanie nowej linii w tabeli w pliku 'TestSuite.html' znajdującym się w tym samym katalogu. Listing 15-37 pokazuje jak to zrobić.

*Listing 15-37 - Dodanie pliku testu do zestawu testów w web/selenium/test/TestSuite.html*

...
<tr><td><a href='./testIndex.html'>Mój pierwszy test</a></td></tr>
...

Aby uruchomić test, po prostu wpisz w przeglądarce:

http://myapp.example.com/selenium/index.html

Wybierz Main Test Suite, wciśnij przycisk, aby uruchomić wszystkie testy i obserwuj przeglądarkę jak odtwarza kroki, które nakazałeś.

NOTE Ponieważ testy Selenium są uruchamiane w prawdziwej przeglądarce, pozwalają również sprawdzać niezgodności przeglądarek. Utwórz testy z użyciem jednej przeglądarki i przetestuj je na wszystkich pozostałych, na których powinna działać twoja strona.

Ponieważ testy Selenium są napisane w HTML, ich pisanie może być mordęgą. Ale dzięki rozszerzeniu Selenium dla Firefoxa (http://seleniumrecorder.mozdev.org/) stworzenie testu polega na jego zarejestrowaniu w nagrywanej sesji. Podczas nawigacji w rejestrowanej sesji, możesz dodać sprawdzanie asercji poprzez kliknięcie prawym klawiszem myszki w oknie przeglądarki i wybranie odpowiedniej opcji w menu kontekstowym Selenium.

Możesz zapisać test w pliku HTML aby utworzyć zestaw testów dla twojej aplikacji. Rozszerzenie Firefoxa pozwala nawet na uruchomienie testu Selenium, który zarejestrowałeś.

NOTE Nie zapomnij ponownie zainicjować danych testowych zanim uruchomisz test Selenium.

Podsumowanie

Zautomatyzowne testy obejmują testy jednostkowe sprawdzające działanie funkcji i metod oraz testy funkcjonalne sprawdzające pewne funkcjonalności aplikacji. Symfony opiera się na frameworku testowym lime dla testów jednostkowych i dostarcza klasę 'sfTestBrowser' specjalnie do testów funkcjonalnych. W obu przypadkach dostępnych jest wiele metod do sprawdzania asercji, od podstawowych do najbardziej zaawansowanych, jak selektory CSS. Używaj konsolowych poleceń Symfony, aby uruchamiać testy pojedynczo (za pomocą poleceń 'test-unit' i 'test-functional') lub w frameworku uruchomieniowym (za pomocą polecenia 'test-all'). Przy pracy z danymi, zautomatyzowane testy wykorzystują namiastki i dane testowe, co jest łatwe do osiągnięcia w testach jednostkowych Symfony.

Jeżeli przekonasz się do pisania testów jednostkowych pokrywającących znaczną część kodu twoich aplikacji (na przykład używając metodologii TDD), możesz poczuć się bezpieczniej w trakcie refaktoryzacji lub dodawania nowych funkcjonalności, a nawet zyskać trochę czasu na tworzeniu dokumentacji.