Więcej na rubyonrails.pl: Start | Pobierz | Wdrożenie | Kod (en) | Screencasty | Dokumentacja | Ekosystem | Forum | IRC

Przewodnik testowania aplikacji Railsowych

Ten przewodnik omawia wbudowane mechanizmy oferowane przez Railsy, służące do testowania aplikacji. Po zapoznaniu się z tym przewodnikiem, będziesz w stanie:

Ten przewodnik nie nauczy cię pisać aplikacji Railsowych; zakładamy podstawową znajomość tworzenia rzeczy w Railsach.

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 raketest 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

Lighthouse ticket

  • 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)