1 Dlaczego powinniśmy pisać testy do aplikacji Railsowych?
- Railsy umożliwiają bardzo łatwe pisanie testów. Rozpoczynamy od pisania szkieletu kodu testowego w tle tworzenia modeli oraz kontrolerów.
- Poprzez uruchomienie testów Railsów możemy zapewnić, że kod spełnia wymaganą funkcjonalność nawet po dużych jego zmianach
- Testy Railsów mogą także symulować zapytania przeglądarki, zatem możemy testować odpowiedzi aplikacji bez testowania ich w przeglądarce
2 Wprowadzenie do testowania
Wsparcie testowania było zaimplementowane w architekturze Railsów od samego początku. Nie było to objawienie w stylu: “Oh! Stwórzmy wsparcie dla testowania aplikacji, ponieważ to jest nowe i cool”. Niemalże każda aplikacja Railsowa ściśle oddziałuje z bazą danych i w rezultacie, testy takze muszą z nią oddziaływać. Aby pisać sprawnie działające testy, musisz zrozumieć jak stworzyć bazę danych oraz zaludnić ją przykładowymi danymi.
2.1 Trzy środowiska
Każda aplikacja Railsowa, którą budujesz ma 3 strony: stronę produkcji, stronę developmentu oraz testowania.
To rozróżnienie można odnaleźć w jednym miejscu, mianowicie w pliku: config/database.yml file. Ten plik konfiguracyjny YAML ma 3 różne sekcje definiujące ustawienia bazy:
- produkcja
- development
- testowanie
To pozwala na ustawienie oraz oddziaływanie z danymi testowanymi bez jakiegokolwiek zagrożenia w postaci zmodyfikowania danych produkcyjnych poprzez dane testowe.
Na przykład, jeżeli potrzebujesz przetestować nową funkcję skasuj_tego_użytkownika_i_wszystko_z_nim_powiązane. Czy nie chciałbyś przeprowadzić tego testu w środowisku, w którym nie ma znaczenia czy zniszczysz dane czy nie?
Jeśli zniszczysz swoją bazę testową (a to na pewno będzie miało miejsce, uwierzcie mi), możesz bez problemu odbudować ją odnosząc się do specyfikacji zdefiniowanych w bazie developerskiej. Można to zrobić za pomocą polecenia rake db:test:prepare.
2.2 Railsy przygotowywują się do testowania od pierwszej komendy
Railsy tworzą folder test gdy tylko utworzysz nowy projekt Railsowy poleceniem rails _nazwa_aplikacji. Jeśli wylistujemy zawartość tego katalogu zobaczymy:
$ ls -F test/
fixtures/ functional/ integration/ test_helper.rb unit/
Folder unit przechowywuje testy dla Twoich modelów, folder functional testy dla kontrolerów, zaś folder integration testy, które zawierają jakiekolwiek współpracujące ze sobą kontrolery. Fixtures to sposób organizowania testowanych danych – znajdują się one w folderze fixtures. Plik test_helper.rb przechowywuje domyślną konfigurację testów.
2.3 O Fixtures
Dla efektywnych testów, należy zastanowić się nad stworzeniem danych, które będziemy testować. W Railsach można to zrobić za pomocą zdefiniowania i dostosowania Fixtures.
2.3.1 Czym są Fixtures?
Fixtures to wymyślne określenie dla podstawowych danych. Fixtures pozwalają na zapełnienie testowej bazy predefiniowanymi danymi zanim uruchomisz test. Fixtures są niezależne oraz występują w dwóch formatach: YAML lub CSV. W tym przewodniku użyjemy formatu YAML, który jest formatem preferowanym.
Fixtures mogą być znalezione w folderze test/fixtures. W momencie uruchomienia skryptu script/generate model w celu stworzenia nowego modelu, fixtures zostaną automatycznie stworzone oraz umieszczone w tym folderze.
2.3.2 YAML
Fixtures sformatowane za pomocą YAML są bardzo przyjaznym dla człowieka sposobem prezentacji danych. Te typy fixtures posiadają rozszerzenie .yml (np. users.yml).
Poniżej znajduje się przykładowy plik YAML:
# Jestem komentarzem YAML!
david:
name: David Heinemeier Hansson
birthday: 1979-10-15
profession: Systems development
steve:
name: Steve Ross Kellock
birthday: 1974-09-27
profession: guy with keyboard
Każda fixture posiada nazwę, po której następuje lista rozdzielonych przecinkiem par klucz/wartość. Rekordy oddzielone są spacją. W fixtures można tworzyć także komentarze używając znaku w pierwszej linii (#).
2.3.3 O ERb
ERb pozwala na osadzanie kodu ruby w szablonach. Formaty YAML oraz CSV są analizowane przed przetwarzaniem za pomocą ERb, w momencie gdy uruchamiamy fixtures. To pozwala na używanie Ruby do generacji prostych danych.
<% earth_size = 20 -%>
mercury:
size: <%= earth_size / 50 %>
brightest_on: <%= 113.days.ago.to_s(:db) %>
venus:
size: <%= earth_size / 2 %>
brightest_on: <%= 67.days.ago.to_s(:db) %>
mars:
size: <%= earth_size - 69 %>
brightest_on: <%= 13.days.from_now.to_s(:db) %>
Cokolwiek zamieszczone w znacznikach
<% %>
jest uważane za kod Ruby. Kiedy fixtures są ładowane, atrybut size powyższych trzech rekordów zostanie ustawiony na 20/50, 20/2 oraz 20-69. Atrybut brightest_on także zostanie sprawdzony i sformatowany przez Railsy, aby był kompatybilny z bazą danych.
2.3.4 Fixtures w akcji
Rails domyślnie automatycznie ładuje wszystkie fixtures znajdujące się w katalogu ‘test/fixtures’ dla testów jednostki i funkcjonalności. Ładowanie to zawiera trzy kroki:
- Usuwanie wszystkich istniejących danych w tabeli nawiązujących do fixture
- Ładowanie danych fixture do tabeli
- Wrzucenie danych fixture do zmiennej (o ile chcesz bezpośrednio z niej korzystać)
2.3.5 Hashe o specjalnej mocy
Fixtures są obiektami Hash. Jak wspomniano w punkcie #3 powyżej, możemy uzyskać dostęp do obiektu hash bezpośrednio ponieważ jest on automatycznie ustawiany jako zmienna lokalna dla przypadku tekstowego. Na przykład:
# to polecenie zwróci Hash dla fixture nazwanej david
users(:david)
# to polecenie zwróci własność david nazwaną id
users(:david).id
Fixtures mogą także transformować w formy orginalnej klasy. Dzięki temu możemy dostać się do metod dostępnych tylko dla tej klasy.
# używając metody find, wybieramy "prawdziwego" davida jako Użytkownika (User)
david = users(:david).find
# teraz mamy dostęp do metod dostępnych tylko dla klasy User
email(david.girlfriend.email, david.location_tonight)
3 Jednostkowe testowanie modeli
W Railsach testy jednostki (unit tests) to te testy, które piszemy aby przetestować modele.
W tym przewodniku będziemy używać Railsowego scaffolding. Polecenie to stworzy model, migrację, kontroler oraz widoki dla nowego zasobu pojedynczej operacji. Stworzy także pełny komplet testowy zgodnie z najlepszymi praktykami Rails. Przykłady będziemy czerpać z wygenerowanego kodu oraz kiedy to potrzebne, wzbogacać go dodatkowymi.
Po więcej informacji na temat polecenia scaffolding, zapraszamy do lektury rozdziału Początki w Rails
Gdy używamy polecenia script/generate scaffold stworzony zostaje odcinek w folderze test/unit:
$ script/generate scaffold post title:string body:text
...
create app/models/post.rb
create test/unit/post_test.rb
create test/fixtures/posts.yml
...
Domyślny testowy odcinek w folderze test/unit/post_test.rb wygląda tak:
require 'test_helper'
class PostTest < ActiveSupport::TestCase
# Replace this with your real tests.
test "the truth" do
assert true
end
end
Przeanalizowanie tego kodu linijka po linijce pozwoli Ci na zorientowanie się w terminologii oraz kodach testowych Railsów.
require 'test_helper'
Jak już wiesz test_helper.rb zawiera domyślną konfigurację dla Twoich testów. Ten plik powiązany jest z wszystkimi testami, zatem jakie metody dodasz, będą one dostępne dla wszystkich testów.
class PostTest < ActiveSupport::TestCase
Klasa PostTest definiuje test case ponieważ dziedziczy z ActiveSupport::TestCase. PostTest posiada więc wszystkie metody dostępne dla ActiveSupport::TestCase. Te metody napotkasz w dalszej części przewodnika.
def test_truth
Każda metoda zdefiniowana w test case, która rozpoczyna się od test (rozróżniamy małe i duże litery) jest zwana testem. Zatem test_password, test_valid_password czy testValidPassword są poprawnymi nazwami testów i zostaną uruchomione automatycznie jeśli uruchomimy także test case.
Railsy dodają metodę test, która wyciąga nazwę testu oraz blok. Generuje ona standardowy test Test::Unit z nazwami metod oprefiksowanymi za pomocą wyrażenia test_.
test "the truth" do
# ...
end
To sprawia, że nazwy testów stają się bardziej czytelne poprzez zastąpienie znaku podkreślenia regularnym językiem.
assert true
Ta linijka kodu jest nazywana assertion. Assertion (dowodzenie) to linijka kodu, która sprawdza obiekt (lub wyrażenie) i potencjalne rezultaty jego działania. Na przykład dowodzenie może sprawdzić:
- czy ta wartość = inna wartość?
- czy ten obiekt jest zerem?
- czy ta linijka kodu wyrzuca wyjątek?
- czy hasło użytkownika ma więcej niż 5 znaków?
Każdy test zawiera jedno lub więcej dowodzeń. Tylko w przypadku, gdy wszystkie dowodzenia są udane test można uznać za zdany.
3.1 Przygotowywanie aplikacji do testowania
Zanim uruchomisz testy, musisz się upewnić, że struktura testowej bazy danych jest aktualna. Aby to zrobić możesz użyć poniższych komend:
$ rake db:migrate
...
$ rake db:test:load
Powyższe polecenie rake db:migrate uruchamia wszystkie oczekujące migracje w środowisku development oraz uaktualnia plik db/schema.rb. rake db:test:load tworzy ponownie bazę danych z aktualnego pliku db/schema.rb. Podczas kolejnych prób, dobrą praktyką jest najpierw uruchomić db:test:prepare, który sprawdzi czy istnieją oczekujące migracje i odpowiednio o nich ostrzeże.
db:test:prepare nie powiedzie się jeżeli plik db/schema.rb nie istnieje.
3.1.1 Polecenia rake służące do przygotowania aplikacji do testowania
Polecenie | Opis |
---|---|
rake db:test:clone | Odtworzenie testowej bazy z schematu bazy aktualnego środowiska |
rake db:test:clone_structure | Odtworzenie baz testowych z struktury development |
rake db:test:load | Odtworzenie testowej bazy z aktualnego pliku schema.rb |
rake db:test:prepare | Sprawdzenie czy istnieją oczekujące migracje oraz załadowanie schematu testowego |
rake db:test:purge | Wyczyszczenie testowej bazy |
Możesz zobaczyć wszystkie powyższe polecenia rake wraz z ich opisami poprzez wpisanie rake --tasks --describe.
3.2 Uruchamianie testów
Uruchamianie testu jest równie proste jak wywołanie pliku zawierającego przypadki testów (test case) poprzez Ruby:
$ cd test
$ ruby unit/post_test.rb
Loaded suite unit/post_test
Started
.
Finished in 0.023513 seconds.
1 tests, 1 assertions, 0 failures, 0 errors
To polecenie uruchomi wszystkie metody testów z przypadku testów (test case).
Możemy także uruchomić poszczególny test z przypadku testów poprzez dodanie -n do test method name.
$ ruby unit/post_test.rb -n test_truth
Loaded suite unit/post_test
Started
.
Finished in 0.023513 seconds.
1 tests, 1 assertions, 0 failures, 0 errors
Kropka (.) oznacza zdany test. W przeciwnym wypadku zobaczymy F, zaś kiedy test wyrzuci błąd zobaczymy E w tym samym miejscu. Ostatnia linijka to podsumowanie.
Aby zobaczyć jak raportowana jest porażka testu, możemy dodać taki test do post_test.rb.
test "should not save post without title" do
post = Post.new
assert !post.save
end
Teraz urchomimy nowo dodany test.
$ ruby unit/post_test.rb -n test_should_not_save_post_without_title
Loaded suite -e
Started
F
Finished in 0.102072 seconds.
1) Failure:
test_should_not_save_post_without_title(PostTest) [/test/unit/post_test.rb:6]:
<false> is not true.
1 tests, 1 assertions, 1 failures, 0 errors
F na wyjściu oznacza nie zdanie testu. Możemy zobaczyć odpowiedni ślad pokazany pod numerem 1) razem z nazwą danego testu. Następne pare linijek zawiera stos śladów razem z wiadomością dotyczącą aktualnej wartości oraz wartości oczekiwanej przez dowodzenie (assertion).
test "should not save post without title" do
post = Post.new
assert !post.save, "Saved the post without a title"
end
Uruchomienie tego testu pokazuje wiadomość dowodzenia w bardziej przyjazny sposób:
1) Failure:
test_should_not_save_post_without_title(PostTest) [/test/unit/post_test.rb:6]:
Saved the post without a title.
<false> is not true.
Aby sprawić, że test ten zostanie przeprowadzony poprawnie możemy dodać walidację poziomu modelu dla pola title.
class Post < ActiveRecord::Base
validates_presence_of :title
end
Teraz test powinien się powieść. Zweryfikujmy to poprzez ponowne uruchomienie.
$ ruby unit/post_test.rb -n test_should_not_save_post_without_title
Loaded suite unit/post_test
Started
.
Finished in 0.193608 seconds.
1 tests, 1 assertions, 0 failures, 0 errors
Teraz, jeżeli zauważyłeś na początku napisaliśmy test, który nie powiódł się dla danej funkcjonalności, zaś potem dopisaliśmy kod, który dodaje funkcjonalność i finalnie test powiódł się. To podejście do tworzenia oprogramowania jest określane mianem Test-Driven Development (TDD) (Rozwój poprzez testowanie).
Wiele developerów Railsów praktykuje Test-Driven Development (TDD). To wspaniała droga do budowania kompletu testów, sprawdzających każdą część aplikacji. TDD nie należy jednak do tematów tego przewodnika ale zapoznawanie się z tą tematyką można zacząć od 15 kroków TDD do stworzenia aplikacji Rails.
Aby zobaczyć jak raportowane są błędy, poniżej umieszczamy test zawierający błąd:
test "should report error" do
# jakaś_niezdefiniowana_zmienna nie jest nigdzie zdefiniowana w teście
some_undefined_variable
assert true
end
Teraz możesz zobaczyć jeszcze wiecej danych wyjściowych w konsoli po uruchomieniu testu:
$ ruby unit/post_test.rb -n test_should_report_error
Loaded suite -e
Started
E
Finished in 0.082603 seconds.
1) Error:
test_should_report_error(PostTest):
NameError: undefined local variable or method `some_undefined_variable' for #<PostTest:0x249d354>
/test/unit/post_test.rb:6:in `test_should_report_error'
1 tests, 0 assertions, 0 failures, 1 errors
Zauważ ‘E’ na wyjściu. Oznacza to test z błędem.
Wykonywanie danej metody testu jest zatrzymywane jak tylko jakikolwiek błąd lub niepowodzenie dowodzenia zostanie napotkane. Komplet testów kontynuuje wtedy wykonywanie od następnej metody. Wszystkie metody testowe są wykonywane w kolejności alfabetycznej.
3.3 Co zawrzeć w testach jednostkowych
Idealnie chcielibyśmy mieć test na wszystko co może potencjalnie się zepsuć. Dobrą praktyką jest posiadać przynajmniej jeden test dla każdej walidacji i co najmniej jeden test dla każdej metody w modelu.
3.4 Dostępne asercje
Teraz już znasz niektóre dostępne metody dowodzenia. Metody te są pszczołami robotnicami testowania. To właśnie one dokonują sprawdzania aby upewnić się, że wszystko odbywa się zgodnie z planem.
Istnieje wiele innych typów dowodzenia jakich możesz użyć. Poniżej znajduje się kompletna lista metod zawartych w test/unit, bibliotece testowej używanej przez Railsy. Parametr [msg] jest opcjonalnym ciągiem znaków (wiadomością), którą możesz wyspecyfikować aby komunikaty błędów były bardziej jasne. Nie jest to jednak wymagane.
Metoda dowodzenia | Zadanie |
---|---|
assert( boolean, [msg] ) | Upewnia się, że obiekt/wyrażenie jest prawdziwe. |
assert_equal( obj1, obj2, [msg] ) | Upewnia się, że obj1 == obj2 jest prawdą. |
assert_not_equal( obj1, obj2, [msg] ) | Upewnia się, że obj1 == obj2 jest fałszem. |
assert_same( obj1, obj2, [msg] ) | Upewnia się, że obj1.equal?(obj2) jest prawdą. |
assert_not_same( obj1, obj2, [msg] ) | Upewnia się, że obj1.equal?(obj2) jest fałszem. |
assert_nil( obj, [msg] ) | Upewnia się, że obj.nil? jest prawdą. |
assert_not_nil( obj, [msg] ) | Upewnia się, że obj.nil? jest fałszem. |
assert_match( regexp, string, [msg] ) | Upewnia się, że ciąg znaków pasuje do wyrażenia regularnego. |
assert_no_match( regexp, string, [msg] ) | Upewnia się, że ciąg nie pasuje do wyrażenia regularnego. |
assert_in_delta( expecting, actual, delta, [msg] ) | Upewnia się, że liczby oczekiwane oraz aktualne są w zasięgu delta od siebie. |
assert_throws( symbol, [msg] ) { block } | Upewnia się, że dany blok zwraca symbol. |
assert_raise( exception1, exception2, ... ) { block } | Upewnia się, że dany blok zwraca jeden z danych wyjątków. |
assert_nothing_raised( exception1, exception2, ... ) { block } | Upewnia się, że dany blok nie zwraca żadnego z danych wyjątków. |
assert_instance_of( class, obj, [msg] ) | Upewnia się, że obj jest typem danej klasy. |
assert_kind_of( class, obj, [msg] ) | Upewnia się, że obj należy do lub dziedziczy z class. |
assert_respond_to( obj, symbol, [msg] ) | Upewnia się, że obj ma metodę nazwaną symbol. |
assert_operator( obj1, operator, obj2, [msg] ) | Upewnia się, że obj1.operator(obj2) jest prawdą. |
assert_send( array, [msg] ) | Upewnia się, że wykonanie metody wylistowanej w array[1] na obiekcie z array[0] z parametrami array[2 i więcej niż 2] jest prawdą. Dziwne, nie? |
flunk( [msg] ) | Upewnia się, że test nie powiódł się. To rozwiązanie jest użyteczne do oznaczenia jeszcze niedokończonych testów. |
Ze względu na modularną naturę szkieletu (frameworku) testowania, jest możliwe tworzenie własnych dowodzeń. W zasadzie to właśnie robią Railsy. Aby to ułatwić istnieją pewne wyspecjalizowane mechanizmy.
Tworzenie swoich własnych metod dowodzeń to temat dla zaawansowanych, nie będzie on pokryty w tym przewodniku.
3.5 Rails Specific Assertions
Railsy dodają pewne niestandardowe dowodzenia w strukturze test/unit:
assert_valid(record) zostało zastąpione. Używamy assert(record.valid?) w zamian.
Metoda dowodzenia | Zadanie |
---|---|
assert_valid(record) | Upewnia się, że podany rekord odpowiada standardom Active Record i czy zwraca błąd jeśli nie jest. |
assert_difference(expressions, difference = 1, message = nil) {...} | Testuje numeryczną różnicę pomiędzy zwracaną wartością wyrażenia jako wyniku tego, co było przetworzone w danym bloku. |
assert_no_difference(expressions, message = nil, &block) | Dowodzi, że numeryczna wartość danego wyrażenia nie została zmieniona przed i po wywołaniu przekazania w bloku. |
assert_recognizes(expected_options, path, extras={}, message=nil) | Dowodzi, że trasowanie danej ścieżki zostało wykonane poprawnie i parsowane opcje (podane w hashu expected_options) zgadzają się ze ścieżką. Dowodzi zatem, że Railsy rozpoznają ścieżkę podaną w expected_options. |
assert_generates(expected_path, options, defaults={}, extras = {}, message=nil) | Dowodzi, że zapewnione opcje mogą służyć do generacji zapewnionej ścieżki. To odwrócenie assert_recognizes. Dodatkowy parametr jest używany do podania zapytaniom nazw i wartości dodatkowych porządanych parametrów, które znajdą się w zapytaniu. Parametr message pozwala także na specyfikację niestandardowej wiadomości, pojawiającej się w razie wystąpienia błędu. |
assert_response(type, message = nil) | Dowodzi, że odpowiedzi nadchodzi z danym kodem statusu. Można wyspecyfikować :success wyświetlany przy wartości 200, :redirect przy 300-399, :missing przy 404 oraz :error na 500-599. |
assert_redirected_to(options = {}, message=nil) | Dowodzi, że przekazane opcje przekierowania pasują do tych opcji przekierowania, jakie były użyte w ostatniej akcji. Dopasowanie to może być częściowe, np. assert_redirected_to(:controller => "weblog") będzie pasowało do przekierowania redirect_to(:controller => "weblog", :action => "show") i tym podobne. |
assert_template(expected = nil, message=nil) | Dowodzi, że zapytanie zostało wykonane w odpowiednim pliku szablonu. |
Użycie niektórych z tych metod zobaczycie w kolejnym rozdziale.
4 Funkcjonalne testy kontrolerów
W Railsach testowanie różnych akcji pojedynczego kontrolera jest nazywane pisaniem testów funkcjonalnych dla tego kontrolera. Kontrolery zajmują się przychodzącymi do aplikacji zapytaniami z sieci web i ewentualnie odpowiadają stworzonym widokiem.
4.1 Co zawrzeć w testach funkcjonalnych
Powinno się testować takie rzeczy jak:
- czy zapytanie z internetu powiodło się?
- czy użytkownik został przekierowany do odpowiedniej strony?
- czy użytkownik został z powodzeniem zautoryzowany?
- czy poprawny obiekt został umieszczony w szablonie wysyłanym w odpowiedzi?
- czy użytkownikowi wyświetlono odpowiedni komunikat?
Teraz, kiedy użyliśmy Railsowego generatora scaffold dla naszego źródła Post, kod kontrolera oraz funkcjonalne testy zostały już utworzone. Możesz zobaczyć plik posts_controller_test.rb w folderze test/functional.
Pozwólcie, że zaprezentuję jeden taki test – test_powinien_pobrac_index (test should get index) z pliku posts_controller_test.rb.
test "should get index" do
get :index
assert_response :success
assert_not_nil assigns(:posts)
end
W teście test_powinien_pobrac_index, Railsy symulują zapytanie na akcji nazwanej index, upewniając się, że zapytanie zostało zakończone powodzeniem oraz przypisano poprawną instancję posts.
Metoda get rozpoczyna od zapytania z sieci web a następnie dodaje informacje będące rezultatami formułując z nich odpowiedź. Akceptuje 4 argumenty:
- Akcja kontrolera, jaki wywołujemy. Może mieć ona formę symbolu lub ciagu znaków (string).
- Opcjonalny hash wywoływanych parametrów do przekazania akcji (na przykład ciąg znaków będący zapytaniem lub zmienne post).
- Opcjonalny hash zmiennych sesji do przekazania razem z zapytaniem.
- Opcjonalny hash wartości flash.
Na przykład: wywołanie akcji :show, przekazanie id 12 jako params oraz ustawienie user_id na 5 w sesji:
get(:show, {'id' => "12"}, {'user_id' => 5})
Kolejny przykład: wywołanie akcji :view, przekazanie id 12 jako params, tym razem bez sesji, a z wiadomością flash.
get(:view, {'id' => '12'}, nil, {'message' => 'booya!'})
Jeśli próbujesz uruchomić test test_should_create_post z post_controller_test.rb jest to działanie skazane na porażkę z powodu nowo dodanego modelu walidacji.
Zmodyfikujmy zatem test test_should_create_post w posts_controller_test.rb tak, aby test zakończył się powodzeniem:
test "should create post" do
assert_difference('Post.count') do
post :create, :post => { :title => 'Some title'}
end
assert_redirected_to post_path(assigns(:post))
end
Teraz możesz spróbować uruchomić wszystkie testy – powinny zakończyć się powodzeniem.
4.2 Dostępne typy zapytań dla testów funkcjonalnych
Jeśli jesteś zaznajomiony z protokołem HTTP wiesz, że get jest typem zapytania. Istnieje 5 rodzajów zapytań wspomaganych przez funkcjonalne testy Railsów:
- get
- post
- put
- head
- delete
Wszystkie typy zapytań są metodami jakich możesz użyć, jednakże pewnie będziesz używał pierwszych dwóch o wiele częściej niż pozostałych.
4.3 Cztery hashe apokalipsy
Po tym jak zapytanie zostanie wysłane używając jednej z 5 metod (post, get, itp.) i przetworzone mamy do dyspozycji 4 obiekty hash:
- assigns – każdy obiekt przechowywany jako instancja zmiennej w akcji do użycia w widoku.
- cookies – każdy plik cookie (ciasteczko) jaki jest ustawiony.
- flash – każdy obiekt istniejący we flashu.
- session – każdy obiekt istniejący w zmiennych sesyjnych.
Tak jak w przypadku z normalnymi zmiennymi typu Hash, możesz mieć dostęp do wartości poprzez odwołanie do kluczy przy użyciu ciągu znaków. Możesz także odwoływać się do nich poprzez nazwę symbolu, oprócz assigns. Na przykład:
flash["gordon"] flash[:gordon]
session["shmession"] session[:shmession]
cookies["are_good_for_u"] cookies[:are_good_for_u]
# Nie można używać assings[:coś] ze względów historycznych:
assigns["something"] assigns(:something)
4.4 Dostępne instancje zmiennych
W swoich testach funkcjonalnych możesz także uzyskać dostęp do trzech instancji zmiennych:
- @controller – kontroler przetwarzający zapytanie
- @request – zapytanie
- @response – odpowiedź
4.5 Pełniejszy przykład testu funkcjonalnego
Poniżej znajduje się kolejny przykład, który używa flash, assert_redirected_to oraz assert_difference:
test "should create post" do
assert_difference('Post.count') do
post :create, :post => { :title => 'Hi', :body => 'This is my first post.'}
end
assert_redirected_to post_path(assigns(:post))
assert_equal 'Post was successfully created.', flash[:notice]
end
4.6 Testowanie wyglądu
Testowanie odpowiedzi na zapytanie poprzez dowodzenie obecności kluczowych elementów HTML oraz ich treści jest użyteczną drogą testowania wyglądu aplikacji. Dowodzenie assert_select pozwala na uczynienie tego za pomocą bardzo prostej ale potężnej składni.
Możesz odnaleźć referencje do assert_tag w innej dokumentacji, ale to polecenie jest teraz zastępowane przez assert_select.
Istnieją dwie formy assert_select:
assert_select(selector, [equality], [message]) zapewnia, że warunek równości jest spełniony na wybranych poprzez selektor elementach. Selektor może być wyrażeniem CSS (string), wyrażeniem z zmiennymi podstawionymi lub obiektem HTML::Selector.
assert_select(element, selector, [equality], [message]) zapewnia, że warunek równości jest spełniony na wybranych poprzez selektor elementach zaczynając od element (instancja HTML::Node) oraz jego potomkach.
Przykładowo, możemy zweryfikować zawartość tytułu elementu:
assert_select 'title', "Welcome to Rails Testing Guide"
Możemy także użyć zagnieżdżonego bloku assert_select. W tym przypadku wewnętrzne assert_select działa wewnątrz metody dowodzenia na kompletnej kolekcji elementów wybranych przez zewnętrzne assert_select.
assert_select 'ul.navigation' do
assert_select 'li.menu_item'
end
Alternatywnie, kolekcja elementów wybrana przez zewnętrzne assert_select może być powtórzone w taki sposób, że assert_select będzie mogło być wywołane oddzielnie dla każdego elementu. Załóżmy na potrzeby przykładu, że odpowiedź zawiera dwie uporządkowane listy, każda z 4 elementami o charakterze listy – obydwa testy powiodą się.
assert_select "ol" do |elements|
elements.each do |element|
assert_select element, "li", 4
end
end
assert_select "ol" do
assert_select "li", 8
end
Dowodzenie assert_select jest dosyć potężne. Po bardziej zaawansowane użycie odsyłam do dokumentacji.
4.6.1 Dodatkowe dowodzenia oparte na widoku
Istnieje wiecej dowodzeń, które są używane w testowaniu wyglądu:
Dowodzenie | Zadanie |
---|---|
assert_select_email | Pozwala na tworzenie dowodzeń w treści wiadomości e-mail. |
assert_select_rjs | Pozwala na tworzenie dowodzeń na podstawie odpowiedzi RJS. assert_select_rjs posiada zmienne, które pozwalają zawężyć działania do aktualizowanego elementu lub nawet danej operacji na elemencie. |
assert_select_encoded | Pozwala na tworzenie dowodzeń na kodzie HTML. Dzieje się to poprzez odkodowanie treści każdego elementu a następnie odwołania do bloku z wszystkimi odkodowanymi elementami. |
css_select(selector) or css_select(element, selector) | Zwraca tablicę elementów wybranych przez selector. W drugim wariancie najpierw dopasowywuje bazowy element oraz próbuje dopasować selector do każdego jego potomka. Jeśli nie ma żadnych dopasowań obydwa warianty zwracają pustą tablicę. |
Poniżej przedstawiamy przykład użycia assert_select_email:
assert_select_email do
assert_select 'small', 'Please click the "Unsubscribe" link if you want to opt-out.'
end
5 Testy integracyjne
Testy integracyjne są używane do testowania interakcji pomiędzy jakąkolwiek liczbą kontrolerów. Są one używane do testowania ważnych przepływów pracy wewnątrz aplikacji.
W odróżnieniu do testów funkcjonalnych czy jednostki, testy integracyjne muszą być stworzone dokładnie w folderze ‘test/integration’ aplikacji. Railsy zapewniają generator tworzący szkielet testów integracyjnych.
$ script/generate integration_test user_flows
exists test/integration/
create test/integration/user_flows_test.rb
Oto jak wygląda świeżo wygenerowany test integracji:
require 'test_helper'
class UserFlowsTest < ActionController::IntegrationTest
# fixtures :your, :models
# Replace this with your real tests.
test "the truth" do
assert true
end
end
Testy integracji dziedziczą z ActionController::IntegrationTest. To sprawia, że pewne dodatkowe narzędzia pomocnicze są dostępne dla tych właśnie testów. W dodatku należy zawrzeć pewne fixtures, abyśmy mieli co testować.
5.1 Narzędzia pomocnicze dla testów integracji
Oprócz standardowych narzędzi wspomagających testowanie, są dostępne także dodatkowe narzędzia wspierające testy integracji:
Narzędzie | Zadanie |
---|---|
https? | Zwraca true jeśli sesja naśladuje bezpieczne zapytanie HTTPS. |
https! | Pozwala na naśladowanie bezpiecznego zapytania HTTPS. |
host! | Pozwala na ustawienie nazwy hosta, która będzie użyta w następnym zapytaniu. |
redirect? | Zwraca true jeżeli ostatnie zapytanie było przekierowaniem. |
follow_redirect! | Śledzi pojedynczą bezpośrednią odpowiedź. |
request_via_redirect(http_method, path, [parameters], [headers]) | Pozwala na tworzenie zapytania HTTP i śledzenie kolejnych przekierowań. |
post_via_redirect(path, [parameters], [headers]) | Pozwala na tworzenie zapytania HTTP POST i śledzenie kolejnych przekierowań. |
get_via_redirect(path, [parameters], [headers]) | Pozwala na tworzenie zapytania HTTP GET i śledzenie kolejnych przekierowań. |
put_via_redirect(path, [parameters], [headers]) | Pozwala na tworzenie zapytania HTTP PUT i śledzenie kolejnych przekierowań. |
delete_via_redirect(path, [parameters], [headers]) | Pozwala na tworzenie zapytania HTTP DELETE i śledzenie kolejnych przekierowań. |
open_session | Otwiera nową zmienną sesji. |
5.2 Przykłady testów integracyjnych
Prosty test integracyjny sprawdzający wiele kontrolerów:
require 'test_helper'
class UserFlowsTest < ActionController::IntegrationTest
fixtures :users
test "login and browse site" do
# login poprzez https
https!
get "/login"
assert_response :success
post_via_redirect "/login", :username => users(:avs).username, :password => users(:avs).password
assert_equal '/welcome', path
assert_equal 'Welcome avs!', flash[:notice]
https!(false)
get "/posts/all"
assert_response :success
assert assigns(:products)
end
end
Jak widzisz test integracyjny zawiera wiele kontrolerów oraz sprawdza cały stos z bazy danych do dyspozytora. W dodatku w teście możemy jednocześnie otworzyć wiele instacji sesji jednocześnie i rozszerzyć te instancje poprzez metody dowodzenia aby stworzyć bardzo potężny testowy DSL (domain-specific language) tylko dla potrzeb naszej aplikacji.
Poniżej znajduje się przykład wielu sesji i niestandardowego DSL w teście integracyjnym
require 'test_helper'
class UserFlowsTest < ActionController::IntegrationTest
fixtures :users
test "login and browse site" do
# User avs loguje się
avs = login(:avs)
# User guest loguje się
guest = login(:guest)
# obydwoje dostępni pod różnymi sesjami
assert_equal 'Welcome avs!', avs.flash[:notice]
assert_equal 'Welcome guest!', guest.flash[:notice]
# User avs może oglądać stronę
avs.browses_site
# User guest też może oglądać stronę
guest.browses_site
# Kontynuuj z pozostałymi dowodzeniami
end
private
module CustomDsl
def browses_site
get "/products/all"
assert_response :success
assert assigns(:products)
end
end
def login(user)
open_session do |sess|
sess.extend(CustomDsl)
u = users(user)
sess.https!
sess.post "/login", :username => u.username, :password => u.password
assert_equal '/welcome', path
sess.https!(false)
end
end
end
6 Polecenia rake służące do uruchamiania testów
Nie musisz konfigurować i uruchamiać każdego osobnego testu. Railsy posiadają wiele komend rake aby wspomóc testowanie. Tabela poniżej pokazuje wszystkie komendy rake wchodzące w skład domyślnego Rakefile, w momencie zainicjowania projektu Rails.
Komenda | Opis |
---|---|
rake test | Uruchamia wszystkie testy – jednostki, funkcjonalne i integracyjne. Możesz także uruchomić samo rake – test jest tutaj domyślne. |
rake test:units | Uruchamia wszystkie testy jednostek z test/unit |
rake test:functionals | Uruchamia wszystkie testy funkcjonalne z test/functional |
rake test:integration | Uruchamia wszystkie testy integracyjne z test/integration |
rake test:recent | Testuje ostatnie zmiany |
rake test:uncommitted | Uruchamia wszystkie jeszcze nie uruchomione testy. |
rake test:plugins | Uruchamia wszystkie testy z vendor/plugins/*/**/test (lub wyspecyfikowanych poprzez PLUGIN=_name_) |
7 Szybka notka o Test::Unit
Ruby posiada wiele bibliotek. Jeden mały gem należący do biblioteki to Test::Unit, framework do testowania jednostki w Ruby. Wszystkie podstawowe metody dowodzenia przedyskutowane wyżek są zdefiniowane w Test::Unit::Assertions. Klasa ActiveSupport::TestCase, której używaliśmy w testach jednostkowych bądź funkcjonalności jest rozwinięta poprzez Test::Unit::TestCase. Dzięki temu możemy używać wszystkich podstawowych metod dowodzenia w naszych testach.
Po więcej informacji na temat Test::Unit, odsyłam do lektury dokumentacja test/unit
8 Ustawienie i rozbicie
Jeśli chcesz uruchomić pewien blok kodu przed startem każdego testu oraz blok kodu po każdym teście masz do dyspozycji dwa specjalne mechanizmy ‘oddzwaniania’. Aby je zrozumieć popatrzmy na przykład testu funkcjonalnego w kontrolerze Posts:
require 'test_helper'
class PostsControllerTest < ActionController::TestCase
# wywoływane przed każdym testem
def setup
@post = posts(:one)
end
# wywoływane po każdym teście
def teardown
# ponieważ reinicjujemy @post przed każdym testem
# ustawianie jej tutaj na zero nie jest niezbędne ale mam nadzieję
# że zrozumiecie jak można używać tej metody
@post = nil
end
test "should show post" do
get :show, :id => @post.id
assert_response :success
end
test "should destroy post" do
assert_difference('Post.count', -1) do
delete :destroy, :id => @post.id
end
assert_redirected_to posts_path
end
end
Powyżej metoda setup jest wywoływana przed każdym testem, więc @post jest dostępny dla każdego testu. Railsy implementują setup oraz teardown jako ActiveSupport::Callbacks. To znaczy, że możesz użyć nie tylko setup i teardown jako metodę w swoich testach. Możesz wyspecyfikować je jako:
- blok
- metodę (jak w poprzednim przykładzie)
- nazwę metody jako symbol
- lambdę
Zobaczmy poprzedni przykład specyfikując setup jako nazwę metody jako symbol:
require '../test_helper'
class PostsControllerTest < ActionController::TestCase
# wywoływane przed każdym testem
setup :initialize_post
# wywoływane po każdym teście
def teardown
@post = nil
end
test "should show post" do
get :show, :id => @post.id
assert_response :success
end
test "should update post" do
put :update, :id => @post.id, :post => { }
assert_redirected_to post_path(assigns(:post))
end
test "should destroy post" do
assert_difference('Post.count', -1) do
delete :destroy, :id => @post.id
end
assert_redirected_to posts_path
end
private
def initialize_post
@post = posts(:one)
end
end
9 Testowanie ścieżek
Jak wszystko inne w aplikacji Railsowej, polecamy testować scieżki. Przykładowy test ścieżki w domyślnej akcji kontrolera Posts wygląda tak:
test "should route to post" do
assert_routing '/posts/1', { :controller => "posts", :action => "show", :id => "1" }
end
10 Testowanie mailingu
Testowanie klas mailingowych wymaga pewnych specyficznych narzędzi.
10.1 Sprawdzanie listonosza
Twoje klasy ActionMailer jak każda część aplikacji Railsowej powinny być przetestowane w celu sprawdzenia ich działania.
Celem testowania klas ActionMailer jest zapewnienie:
- przetwarzania wiadomości e-mail (stworzonych i wysłanych)
- poprawności treści wiadomości e-mail (nadawca, odbiorca, treść, temat)
- wysyłania danych wiadomości o wyznaczonym czasie
10.1.1 Ze wszystkich stron
Istnieją dea aspekty testowania mailera – testy jednostki oraz funkcjonalne. W testach jednostkowych uruchamiamy mailera w izolacji z ściśle kontrolowanymi danymi wejściowymi oraz porównujemy dane wyjściowe do znanej wartości (fixtures – yay! więcej fixtures). W testach funkcjonalnych nie testujemy detali produkowanych przez mailera ale czy kontrolery oraz modele używają go w odpowiedni sposób. Testujemy aby dowieść że wyznaczony e-mail został wysłany w wyznaczonym czasie.
10.2 Testowanie jednostki
Aby testować czy mailer działa tak, jak oczekiwano, możemy użyć testów jednostki aby porównać aktualne wyniki mailera z predefiniowanymi przykładami tego, co powinno zostać wyprodukowane.
10.2.1 Zemsta fixtures
Ze względów na to, że testujemy mailera, fixtures służą do zapewnienia przykładu jak powininny wyglądać dane wyjściowe. Ponieważ są to przykładowe e-maile, a nie dane Active Record jak inne fixtures, są one przechowywane we własnym podkatalogu z dala od innych fixtures. Nazwa tego katalogu jest ściśle związana z nazwą mailera. Zatem jeśli nazywa się on UserMailer to fixtures powinny znajdować się w test/fixtures/user_mailer.
Kiedy generujesz mailera, generator tworzy fixtures dla każdej jego akcji. Jeśli nie użyłeś generatora będziesz zmuszony stworzyć te pliki na własną rękę.
10.2.2 Podstawowy przypadek testowy
Poniżej mamy jednostkę służącą do testu mailera nazwanego UserMailer, którego akcja invite jest używana do wysyłania zaproszenia do przyjaciela. Jest to zaadaptowana wersja podstawowego testu stworzona przez generator dla akcji invite.
require 'test_helper'
class UserMailerTest < ActionMailer::TestCase
tests UserMailer
test "invite" do
@expected.from = 'me@example.com'
@expected.to = 'friend@example.com'
@expected.subject = "You have been invited by #{@expected.from}"
@expected.body = read_fixture('invite')
@expected.date = Time.now
assert_equal @expected.encoded, UserMailer.create_invite('me@example.com', 'friend@example.com', @expected.date).encoded
end
end
W tym teście @expected jest instancją TMail::Mail, której możesz użyć w swoich testach. Jest zdefiniowana w ActionMailer::TestCase. Powyższy test używa @expected aby skonstrułować wiadomość, którą później wyraża poprzez e-mail stworzony przez niestandardowy mailer. Fixture invite znajduje się w treści wiadomości i jest używana jako przykładowa treść. Zmienna pomocnicza read_fixtures odczytuje treść tego pliku.
Poniżej treść fixture invite:
Hi friend@example.com, You have been invited. Cheers!
This is the right time to understand a little more about writing tests for your mailers. The line ActionMailer::Base.delivery_method = :test in config/environments/test.rb sets the delivery method to test mode so that email will not actually be delivered (useful to avoid spamming your users while testing) but instead it will be appended to an array (ActionMailer::Base.deliveries).
However often in unit tests, mails will not actually be sent, simply constructed, as in the example above, where the precise content of the email is checked against what it should be.
10.3 Testowanie funkcjonalne
Functional testing for mailers involves more than just checking that the email body, recipients and so forth are correct. In functional mail tests you call the mail deliver methods and check that the appropriate emails have been appended to the delivery list. It is fairly safe to assume that the deliver methods themselves do their job You are probably more interested in is whether your own business logic is sending emails when you expect them to got out. For example, you can check that the invite friend operation is sending an email appropriately:
require 'test_helper'
class UserControllerTest < ActionController::TestCase
test "invite friend" do
assert_difference 'ActionMailer::Base.deliveries.size', +1 do
post :invite_friend, :email => 'friend@example.com'
end
invite_email = ActionMailer::Base.deliveries.first
assert_equal invite_email.subject, "You have been invited by me@example.com"
assert_equal invite_email.to[0], 'friend@example.com'
assert_match /Hi friend@example.com/, invite_email.body
end
end
11 Inne podejścia do testowania
Testowanie bazujące na wbudowanym test/unit nie jest jedynym sposobem testowania aplikacji Railsowych. Developerzy Railsów wymyślili wiele różnych podejść do testowania, w tym:
- NullDB, przyśpieszanie testowania poprzez unikanie użycia baz danych.
- Factory Girl, jako zastępca fixtures.
- Machinist, kolejny zastępcja fixtures.
- Shoulda, rozszerzenie do test/unit z dodatkowymi narzędzami pomocniczymi, makrami oraz mechanizmami dowodzenia.
- RSpec, framework kierowany zachowaniami.
12 Changelog
- November 13, 2008: Revised based on feedback from Pratik Naik by Akshay Surve (not yet approved for publication)
- October 14, 2008: Edit and formatting pass by Mike Gunderloy (not yet approved for publication)
- October 12, 2008: First draft by Akshay Surve (not yet approved for publication)