Testujeme chybové stavy Rails aplikací
Pokud v Rails testujete fakt důkladně, tak stejně jako já občas narazíte na záludnosti, které není jenom tak jednoduché vyřešit. Testování chybobých stavů je dobrý příklad a vede ke spoustě zajímavých závěrů.
Všechno to začalo pokusem o testování chybových stránek pro stavy 404 (dokument nenalezen) a 500 (vnitřní chyba serveru). Jedná se o stránky, které aplikace zobrazuje dostane-li nezpracovatelný požadavek v prvním případě a dojde-li k neočekávané chybě v případě druhém. Rails v těchto případech většinou renderují statické stránky v public/404.html a public/500.html. Jak se dají lépe handlovat chyby tu rozebírat nebudu. Pro zobrazování pěkných 404 stránek se používá následující položka v routes.rb:
map.all_paths '*path',
:controller => 'site',
:action => 'error_404'
Tahle routa sežere úplně všechno, takže musí být poslední. Její testování taky není úplně jednoduché. Následující kód:
def test_404
get :nejaka_enexistujici_akce
assert_response :missing, "Vyrenderovano s chybou 404"
end
Nezafunguje, protože se prostě nedá namapovat na žádnou akci. Jak tedy ale nato? Je potřeba otestovat, že nějaká nesmyslná route mapuje na naši chybovou akci a pak zkotrolovat, že ta akce vrací 404.
# error_states_test.rb
PATH_404 = "an/url_that/will/raise_404"
PARAMS_404 = PATH_404.split("/")
def test_404
assert_recognizes( {:controller => "site",
:action => "error_404",
:path => PARAMS_404 },
PATH_404)
get :error_404, :path => PARAMS_404
assert_response :missing, "Vyrenderovano s chybou 404"
end
S testováním stránky pro chybu 500 je to daleko zajímavější. Jak testovat situaci, kdy nastane vyjímka? Jak testovat situaci, které se včechny ostatní testy snaží zabránit? V případě, že aplikace používá defaultní routování Rails stačí přidat akci k nějakému existujícímu controlleru. V případě, že defaultní routy nepoužíváte je možné předefinovat metodu pro nějakou existující akci:
# error_states_test.rb
class SiteController
def action_with_error
raise "This Error is expected"
end
def sitemap # normally this displays site-map
raise "This Error is expected"
end
end
class ErrorStatesTest <Test::Unit::TestCase
def setup
@controller = SiteController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
def test_500
get :action_with_error # nebo get :sitemap
assert_response :error, "Vyrenderovano s chybou 500"
end
end
Ať už mám vlastní named routes nebo defaultní routy právě jsem tvrdě narazil. Zde použitý SiteController je již testován v jiném testu a tam jistě najdu tento kód:
# Re-raise errors caught by the controller.
class SiteController; def rescue_action(e) raise e end; end
Kód způsobí, že vyhozená chyba nezpůsobí generování chybové stránky, ale proleze normálně do testu, kde způsobí Error. Sranda je, že test jak jsem ho napsal výše sám o sobě funguje dobře, ale jakmile ho pustím pomocí rake:test tak padá.
Prvním krokem k řešení je existence default routes. Pokud je pro produkční prostřední nechcete (důvodem může bát například SEO), lze je zapnout pouze pro testy a to následovně:
map.connect ':controller/:action/:id' if RAILS_ENV == 'test'
Dále pro testování nějaké obecné funkcionality všech controllerů je potřeba vytvoři nový controller, který bude specifický pro danou sadu testů. Toho se dá docílit jednoduše uvnitř testu, ve kterém testujeme chybové stavy aplikace:
#error_states_test.rb
class ErrorTestController < ApplicationController
# Override an exsting method to raise an error which can
# be expected and tested
def action_with_error
raise "This Error is expected"
end
# Do not override the rescue_action so the application level
# error handling will kick in
end
class ErrorStatesTest <Test::Unit::TestCase
def setup
@controller = ErrorTestController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
def test_500
get :action_with_error
assert_response :error, "Vyrenderovano s chybou 500"
end
end
Poslední háček, který zbývá vyřešit jsou testy Application Controlleru. Následující kód v testu nás totiž pošle zpět do doby ledové:
# application_controller_test.rb
class ApplicationController;
def rescue_action(e) raise e end;
def index
render :text => "Everything is OK"
end
end
Problém vězí v def rescue_action, které se díky dědičnosti použije všude a rozbije nám i ErrorTestController, který tento hack nesmí obsahovat aby korektně reagoval na chybové stavy. Řešení je opět to samé. Vytvoření controlleru specifického pro test:
# application_controller_test.rb
class ApplicationTestController < ApplicationController
def rescue_action(e) raise e end;
def index
render :text => AppConfig[:name]
end
end