assert_select, HTML::Selector en spinnen
Geplaatst door Remco van 't Veer di, 02 jan 2007 19:41:00 GMT
In Rails-1.2 is deassert_tag methode deprecated geworden ipv daarvan hebben we assert_select gekregen. Dat is mooi want van:
assert_tag :tag => 'div', :attributes => {
:class => 'articles'
}, :descendant => {
:tag => 'div', :attributes => {
:id => "article_#{articles(:first).id}"
}, :descendant => {
:tag => 'h1'
}
}assert_select geeft de mogelijkheid deze taal te gebruiken in functionele en integratie tests:
assert_select "div.articles div#article_%s h1" % articles(:first).idIk ben zo enthousiast dat ik zo’n selector ook in m’n test wil gebruiken om links uit m’n HTML te peuteren waarmee ik dan weer andere tests kan doen. Om precies te zijn, ik wil een spider integratie test welke gewoon alle links in m’n applicatie volgt.
Aan de slag!
script/generate integration_test spiderHmm, maar hoe werkt assert_select eigenlijk?
De assert_select methode vind zijn origine in scrAPI van Assaf Arkin, een API om dmv CSS selectors stukjes uit HTML (en andere XML varianten) te schrapen. Een variant van deze API wordt in Rails-1.2 meegeleverd in de vorm van een module genaamd HTML en wordt gebruikt door assert_select.
Onze nieuwe favoriete assert is grof weg als volgt geïmplementeerd:
def assert_select(selector, count = 1)
matches = HTML::Selector.new(selector).select(response_from_page_or_rjs)
assert matches.size == count
endDe response_from_page_or_rjs method is ook nieuw en levert het root element van de resultaat HTML of een speciaal geprepareerd stuk JavaScript. Dat is dus de plek om naar links te gaan vissen voor onze spider:
HTML::Selector.new('a[href]').select(response_from_page_or_rjs)Het bovenstaande levert alle links uit de resultaat HTML. Maar we willen alleen links binnen onze applicatie, ofwel alle relatieve links. Dat is gemakkelijk te testen met de URI class:
URI.parse(link).relative?En bij een link met een hekje, zoals http://test.net/test#test, moet het deel met het hekje eraf gesloopt worden:
link.sub(/#.*$/, '')En dat dan recursief en bijhouden welke links we al gevolgd hebben:
def spider(uri)
get uri
HTML::Selector.new('a[href]').select(response_from_page_or_rjs).each do |anchor|
link = anchor['href'].sub(/#.*$/, '')
if !@visited.include?(link) && URI.parse(link).relative?
@visited << link
spider link
end
end
endMet een assert erin, @visited geïnitialiseerd in de setup methode en de selector herbruikbaar gemaakt, kom ik op de volgende integratie test:
require "#{File.dirname(__FILE__)}/../test_helper"
class SpiderTest < ActionController::IntegrationTest
def test_spider
spider('/')
end
def setup
@visited = []
@selector = HTML::Selector.new('a[href]')
end
def spider(uri)
get uri
assert !response.error?, "#{uri} responded with an error status"
@selector.select(response_from_page_or_rjs).each do |anchor|
link = anchor['href'].sub(/#.*$/, '')
if !@visited.include?(link) && URI.parse(link).relative?
@visited << link
spider link
end
end
end
endMogelijk verbeter punten?
Een beetje feedback zou niet gek zijn. M’n gloed nieuwe MacBook doet deze test in 90 seconden om alle 1500 links te volgen in de applicatie waar ik nu mee bezig ben. Sterker nog, in eerste instantie vond ik een eindeloze loop in m’n routing waarbij de URL steeds langer werden. Een puts uri in het begin van de spider methode?
Zo’n eindeloze loop is natuurlijk gemakkelijk te detecteren door een assert te doen op de lengte van de @visited array;
..
@visited << link
assert @visited.size < 10_000, "endless routing loop?"
..Het is misschien ook wel leuk om alle terug gegeven HTML meteen te valideren op correctheid.

Een spider die alle URL’s binnen een pagina volgt en deze meteen valideerd heb ik al eens geschreven. Het valideren heb ik (quick’n dirty) middels de WWW::Mechanize library op de volgende manier gedaan:
Prachtig! Met de volgende assert in je
test_helperkan je de beschreven spider integratie test gemakkelijk uitbreiden:Of het verstandig is om je test suite afhankelijk te maken van een webservice weet ik eigenlijk niet. Als ik
autotestde hele dag draai, zal ik daarmee duizenden validaties uit laten voeren en dat is misschien niet zo vriendelijk..Het is ook mogelijk om de markup validator service lokaal te draaien, zie: http://validator.w3.org/docs/install.html.
En hoe zit het dan met onclicks?
Welke onclicks? Er zijn twee reden om geen onclicks te doen.
Er horen helemaal geen onclick attributen in je response te zitten probeer CSS, HTML en JavaScript gescheiden te houden.
In het “echte wereld” geval dat je toch onclicks hebt, wat wil je er dan mee doen? Onclicks zijn JavaScript snippets, wil je die dan match op iets als
/document.location\s*=\s*(['"])(.+)\1/en dan groep $2 ook volgen? Ik vind het nix..Bij
<form>’s kan ik me nog wat voorstellen. Gewoon een beetje op submit knoppen rossen is wel een aardige test.