V předchozím dílu jsme se věnovali administrační části galerií. Zobrazit galerii mimo administrační část by myslím už nyní nemělo nikomu činit problémy. Poněvadž je galerie podobná blogům, jednotlivé snímky galerie odpovídají jednotlivým článkům blogu. Liší se pouze ve výpisu a použitých helperech v šabloně.
Co jsme zatím úplně opomíjeli, bylo testování vstupních dat z formulářů.
Kontrola vstupních údajů
Kontrolu vstupních údajů provádí model. Jak to, že ne kontrolér, když „kontroluje”? Vzhledem k MVC architektuře je logicky právě na modelu, aby odmítl uložit položku, která není správně nastavena. Jeden model může být použit ve více kontrolérech. Kdyby mělo k testování docházet v nich, musel by každý obsahovat sadu týchž testů. Ostatně, model můžeme zkoušet i samostatně přes konzoli, přičemž očekáváme, že se bude chovat stejně jako v kontrolérech.
Poďme se podívat na funkční příklad – v předchozích dílech jsme vytvořili model User pomocí Login generátoru. Možná jste si všimli, že při registraci nového uživatele systém upozorní na nesprávně zadané položky. Formulář se v takovém případě po odeslání objeví znovu, tentokrát se zčervenalými políčky. Zkuste si to
http://127.0.0.1:3000/admin/users/signup
Kde se tedy v modelu User provádí kontrola? Na konci souboru user.rb byste měli narazit na následující definice:
app/models/user.rb
...
validates_presence_of :login,
:password,
:password_confirmation
validates_uniqueness_of
:login,
:on => :create
validates_confirmation_of :password
validates_length_of :login,
:within => 3..40
validates_length_of :password,
:within => 5..40, :too_long=>'heslo je prilis dlouhe', :too_short=>heslo je příliš krátké'
Jedná se o zjednodušené zápisy (jakési helpery používané v modelu). Stejnou funkci by zastaly dvě následující metody na místě předchozích helperů (přepis není dokonalý).
def validate_on_create
if self.find_by_login( login )
errors.add( :login, 'uživatel s tímto loginem už v systému existuje' )
end
end
def validate
errors.add( :login, 'login nemůže být prázdný' ) if ! login || login.empty?
errors.add( :password, 'heslo nemůže být prázdné' ) if ! password || password.empty?
errors.add( :password_confirmation, 'potvrzující heslo nemůže být prázdné' ) if ! password_confirmation || password_confirmation.empty?
if password != password_confirm
errors.add( :password, 'heslo se neshoduje s potvrzujícím heslem' )
errors.add( :password_confirmation, 'potvrzující heslo se neshoduje s heslem' )
end
errors.add( :login, 'chybná délka loginu' ) unless (3..40) === login.length
unless (5..40) === login.length
errors.add( :password, 'heslo je prilis kratke' ) if password.length <= 5
errors.add( :password, 'heslo je prilis dlouhe' ) if password.length >= 40
end
end
Většina helperů zná parametry :on a :message, které říkají, pro jakou metodu se má kontrola použít a jaká chyba bude přidána po selhání (errors.add). Další helpery jsou tyto:
- validates_presence_of – testuje, zda je atribut nastaven
validates_presence_of :login
- validate_associated – použijeme tehdy, jsme-li líní – zkontroluje, zda jsou nastaveny všechny atributy, které vyžaduje daný model.
belongs_to :user validate_associated :user
- validate_acceptance_of – aneb bylo zatržítko zatrženo? (daný atribut je roven 1)
validate_accpetance_of :checkbox1, :message => 'zatrhni zatrzitko!'
- validates_format_of – otestujeme atribut pomocí regulárního výrazu
validates_format_of :login, :on => :create, :with => /^[\w\d_-]+$/, :message => 'chybné znaky v loginu'
- validate_numericality_of – testuje, zda je atribut číslem
validate_numericality_of :foo, :only_integer => true
- validates_inclusion_of – testuje, zda je hodnota atributu ve výčtu
validates_inclusion_of :login, :in => [ 'ja', 'on', 'ona' ], :message=> 'nejsi ani ja, ani on, ani ona, takze se nemuzes zaregistrovat; proste nejsi in! ', :on => :create
- validates_exclusion_of – testuje, zda hodnota atributu není ve výčtu
validates_exclusion_of :login, :in => %w{ evil devil jxd }, :message=> 'jdi odkud jsi prisel!', :on => :create
Když selže kontrola údajů v modelu, kontrolér přihlašovacího systému obsahuje kód, který znovu zobrazí formulář. Jakým způsobem ale dochází k obarvení položek a odkud se bere box s chybovými hláškami nad nimi?
O obarvení položek se starají standardní helpery tvořící položky formuláře (v případě chyby přidají class=‚fieldWithErrors‘), celý box s hláškami pak vygeneruje helper <%= error_messages_for ‚user‘ %>. Obvykle stačí upravit kaskádové styly, avšak helpery můžete napsat i vlastní; chyby najdete v proměnné @errors, viz
<%= debug @errors %>
Unit testing
Zabrousíme úplně někam jinam. Sice také půjde o testování, ale testování celé aplikace. Určitě jste si všimli adresáře test (ano, slouží právě k testování). Předpokládáme, že by se každá webová aplikace měla chovat podle nějakých předpokladů. I stane se, že po čase někde něco změníme a nevšimneme si, že se začala chovat jinak – změna cosi nepředvídaně ovlivnila. Abychom tomu předešli, napadne nás vyzkoušet po každé změně, zda vše stále ještě funguje. To je ale časově poněkud náročné. Jaké je nejjednodušší řešení? Napsat sadu testů a po každé změně je všechny provést. Výsledkem bude statistika, která řekne, kolik testů selhalo a proč.
Testování je v Rails rozděleno do dvou částí, na testování modelů a testování kontrolérů. Proto v adresáři test najdeme dva další důležité podadresáře: functional a unit. V podadresáři functional se nalézají testy kontrolérů, v adresáři unit testy modelů.
Testování je prováděno klasicky pomocí tvrzení (assertions). Není to nic zvláštního, jde o volání speciálních metod začínajících assert_, které otestují výraz. Bude-li vyhodnocen jako false, ve výsledné statistice ho uvidíme jako „failure“, vyvolá-li navíc výjimku, zobrazí se ve statistice jako „error“.
Ukažme si testy na příkladu přihlašovacího systému. Díky tomu, že jsme ho vygenerovali, automaticky doplnil patřičné soubory do adresáře test. Budeme testovat kontrolér UsersController. Vzhledem k tomu, že jsme ho po vygenerování přemístili do podadresáře admin, je nutné určité změny provést i v souboru, v němž jsou jeho testy.
Vytvořte podadresář admin a přemístěte tam soubor s testy.
mig> mkdir test/functional/admin
mig> mv test/functional/users_controller_test.rb test/functional/admin
Poté nahraďte v souboru s testy všechny výskyty „UsersController“ za „Admin::UsersController“. Dále upravte cesty (nahoře):
require File.dirname(__FILE__) + '/../../test_helper'
require 'admin/users_controller'
Testování kontroléru bude probíhat tak, že budou simulovány požadavky z formulářů (get či post), a bude vyhodnocováno, jak se kontrolér chová, jinými slovy kam přesměruje, jaká šablona bude použita, atd. Model přitom bude pracovat se speciální, testovací databází, nikoli s vývojovou databází. Testovací databáze vznikne naklonováním vývojové.
vyrobení testovací databáze
Vyrobme nyní tuto testovací databázi. Vzpomeňme si na soubor config/database.yml, v němž jsou databáze definovány, je tam i sekce test, jež by měla vypadat stejně jako development, jen s rozdílným jménem databáze: rolldance_test.
test:
adapter: mysql
database: rolldance_test
username: root
password: 123456
socket: /tmp/mysql.sock
Vytvořme prázdnou databázi
mig> echo 'create database rolldance_test' | mysql -u root --password=123456
a naklonujme do ní vývojovou; automaticky pomocí příkazu rake (rake je něco jako „make“ v Ruby).
mig> rake clone_structure_to_test
Při testování je vhodné mít v tabulkách nějakou základní náplň. Tato náplň musí být vždy před spuštěním testů stejná, aby měly testy stejný výchozí bod. Místo sql souboru jsou v Rails tzv. fixtures, podívejme se na ně do souboru test/fixtures/users.yml. Funguje to tak, že při spuštění testu Rails automaticky nahradí obsah existující tabulky za ten ze souboru.
test/fixtures/users.yml
bob:
id: 1000001
login: bob
password: 9a91e1d8d95b6315991a88121bb0aa9f03ba0dfc # test
existingbob:
id: 1000002
login: existingbob
password: 9a91e1d8d95b6315991a88121bb0aa9f03ba0dfc # test
...
Abych nezapomněl, test spustíme takto:
mig> ruby test/functional/users_controller_test.rb
Loaded suite users_controller_test
Started
.....
Finished in 0.599163 seconds.
5 tests, 24 assertions, 0 failures, 0 errors
No… zamlčel jsem dvě důležité skutečnosti: v souboru test/test_helper.rb musíte mít následující proměnnou nastavenou na true. Rails se vyvíjejí, a co bylo defaultní dříve, dnes již není.
self.use_instantiated_fixtures = true
Teď se konečně budeme zabývat testy kontroléru.
testování kontroléru
test/functional/admin/users_controller_test.rb
def setup
@controller = Admin::UsersController.new
@request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new
@request.host = "localhost"
end
V metodě setup se nejprve vytvoří objekty pseudopožadavku a pseudoodpovědi. Poté jsou spouštěny jednotlivé testy.
def test_auth_bob
@request.session[:return_to] = "/bogus/location"
post :login, :user_login => "bob", :user_password => "test"
assert_session_has :user
assert_equal @bob, @response.session[:user]
assert_redirect_url "/bogus/location"
end
Chtěl bych upozornit na to, že názvy metod test_* nemají nic společného s metodami v opravdovém kontroléru či modelu. Jejich účel je jiný – uzavírají více testů do bloků podle podobné testované funkčnosti. Viz metoda test_bad_signup, která obsahuje více volání post.
Co uvedené testy dělají?
- nastaví se session pseudopožadavku
- simuluje se POST a data jsou předána akci login (v tu chvíli se volá opravdový kontrolér s těmito daty).
- kontrolér musel umístit do session objekt User, otestuje se, zda v session vůbec něco s klíčem :user je
- Jedná se o objekt odpovídající objektu @bob? – To by mělo být splněno, protože akce login na základě zadaných parametrů instancuje nový objekt a uloží ho do session pod klíčem :user.
- Otestuje se, jestli kontrolér přesměroval na adresu /bogus/location. Když se do něj podíváte, provede metodu reditect_back_or_default, která přesměruje na umístění dané v session :return_to.
Mate-li vás, odkud se vzal @bob, je to právě díky use_instantiated_fixtures nastavené na true. Rails automaticky vytvoří instance podle defaultní náplně (fixtures) – users[‚bob‘] a taktéž @bob, users[‚existingbob‘] a též @existingbob, atd.
Testy však stále nedopadají dobře. Opět kvůli tomu, že se Rails vyvíjejí. Metoda assert_redirect_url už není podporována a je nahrazena jiným zápisem:
assert_redirect_url "/bogus/location"
assert_redirect :url=> "/bogus/location"
Teprve teď by mělo vše proběhnout hladce.
testování modelů
Testování kontrolérů je tedy jasné, ale jak je to s testováním modelů? Obdobně. V testovacím modelu test/unit/user_test.rb najdeme obdobné testy na principu tvrzení, na začátku je navíc definován soubor s defaultní náplní databáze.
fixtures :users
Více o použitelných tvrzeních (assertions) najdete v Rails API, v sekci Module Test::Unit::Assertions.
mockování
Při testování modelu se občak hodí předefinovat některé jeho metody. Představme si, že existuje nějaká metoda, která závisí na čemsi externím, co je ale ne vždy dosažitelné. Testy však musejí proběhnout vždy stejně, proto by bylo lepší metodu předefinovat, aby vracela například vždy true (samozřejmě jen při testování). Právě k tomu slouží tzv. mockování. Jinými slovy předefinování původního modelu. V Rails se to dělá jednoduše; stačí do adresáře test/mocks/test nahrát soubor stejného názvu jako je název souboru skriptu modelu. Rails pak načtou namísto původního modelu tento skript a je už jen na něm, aby se zachoval jako skript modelu. Ukážeme si to na příkladu modelu User, v němž předefinujeme jeho metodu create.
test/mocks/test/user.rb
require 'models/user'
class User
def create
return false
end
end
Co „mock” dělá? Nejprve načte původní model, poté předefinuje jeho třídu a metodu create. Redefinice je provedena standardním způsobem, jak ho zná Ruby; stačí třídu a metodu uvést znovu. Zkuste teď výsledek testů, některé by měly selhat.
Mimochodem, kromě toho, že lze spouštět testy jednotlivě jako doposud, lze je též spouštět najednou. Dělá se to pomocí rake, například
mig> rake test_functional
mig> rake test_units
pozn.: list všech příkazů získáte
mig> rake --tasks
Závěr
K tvorbě aplikace lze přistupovat díky testům i opačně – nejprve vymyslet funkčnost, napsat testy a teprve potom vytvořit programový kód. Vyžaduje to ovšem víc přemýšlení, poněvadž aplikace musí nejdříve fungovat v hlavě. Běžně se ale spousta věcí dotváří až v průběhu programování a je otázkou, zda je v analytických schopnostech tvůrce navrhnout funkčnost do všech detailů dopředu. Přesto se ale právě o tento způsob uvažování nyní snažím, výsledkem by mělo být méně chyb v kódu a úspora času při jejich ladění (chce to jen víc přemýšlet). Příště se podíváme na filtry, migrace a kešování.
Aktuální podobu webu Rolldance můžete stáhnout v podobě tarballu.