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).id
Ik 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 spider
Hmm, 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
end
De 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
end
Met 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
end
Mogelijk 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_helper
kan 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
autotest
de 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.