die Qual der Wahl mit Selenium 1

Posted by fwoeck
on Tuesday, May 26

Wie bringe ich meinem Integrationstest – also hier letztlich Selenium – bei, mehrere Optionen eines Multiselect-Feldes auszuwählen? Die vom SeleniumDriver angebotene Methode select sieht so aus:

select(selectLocator,optionLocator)

wobei der optionLocator soweit ich es sehe nur eine(1) Option zulässt. Das mehrmalige Aufrufen nacheinander klappt leider auch nicht, weil die vorher angewählte Option beim neuen Schritt wieder deaktiviert wird.

Dazu ist mir nur eingefallen, den Wert mit JavaScript zu setzen. Hier benutze ich jQuery, weil es in der Website eh benutzt wird. Mit “run_script” lassen sich Skripte unter dem Driver ausführen. In einer Cucumber-Stepsdefinition sieht das dann so aus:

Given /^I choose "([^\"]*)" and "([^\"]*)" from "([^\"]*)"$/ do |opt1, opt2, field|
  selenium.run_script("$('##{field}').val(['#{opt1}','#{opt2}'])")
end

Der val-Methode von jQuery kann man ein Wertearray übergeben.

Weblinks

  1. selenium.rubyforge.org/rdoc/classes/Selenium
  2. jquery-select-elements-tips-and-tricks

Gotcha: Selenium-Tests ohne Netzwerk 0

Posted by fwoeck
on Thursday, May 21

Wenn ich mein Ubuntu-Laptop so ganz ohne Netzwerkverbindung starte – kein LAN/WAN, entsteht ein nerviges Phänomen: der Firefox, der von Selenium bei einem Integrationstest gestartet wird, hält sich für schlau und startet im Offline-Modus.

Kein Problem, würde man denken: es wird ja eh nur auf der 127.0.0.1 getestet. Ha! Auch lokale Seiten lassen sich nicht aufrufen und ergo meldet Selenium einen Sessionfehler.

Das Anlegen von dummy-Gateways, DNS-Servern etc. hat da nichts gebracht (wie kriegt der Browser eigentlich raus, dass kein WAN da ist?). Aber: wenn man den NetworkManager beendet, ist auf einmal alles gut:

sudo /etc/init.d/NetworkManager stop

Selenium, der ruby-Debugger und OsX 0

Posted by fwoeck
on Tuesday, May 12

Die Kombination cucumber/webrat/Selenium macht mir viel Spaß! Es ist sogar möglich während einer Selenium-Sitzung den ruby-debugger zu starten und mit dem Browser zu interagieren. Auf einer Ubuntu-Maschine könnte man folgende Steps definieren:

Given I follow the link "id=vorgangslink" 
And I follow the link "id=p2show" 
And I follow the link "id=formlink_1" 
And I type "ein" into "id=answer_7" 
And I wait 1 second
And the debugger is started
Then I should see "ein String"

Wobei der Debugger dann so aufgerufen wird:

1
2
3
Given /^the debugger is started$/ do
  debugger 
end

Im konkreten Lauf klappt das prima. Wenn man das Gleiche unter OsX tut, wirft das System einem allerdings eine Fehlermeldung um die Ohren, wenn man das selenium-Objekt anspricht, um den Browser zu steuern:

    Given I follow the link "id=vorgangslink" 
    And I follow the link "id=p2show" 
    And I follow the link "id=formlink_1" 
    And I type "ein" into "id=answer_7" 
    And I wait 1 second
/opt/local/lib/ruby/gems/1.8/gems/cucumber-0.3.3/lib/cucumber/ast/step_invocation.rb:33
status!(:passed)

(rdb:1) selenium.type_keys("id=answer_7", "hey")
NameError Exception: undefined local variable or method `selenium' for #<Cucumber::Ast::StepInvocation:0x36c9f28>

(rdb:1) list
[28, 37] in /opt/local/lib/ruby/gems/1.8/gems/cucumber-0.3.3/bin/...
   28          unless @skip_invoke || options[:dry_run] || exception ...
   29            @skip_invoke = true
   30            begin
...

(rdb:1)

Der Grund liegt darin, dass der OsX-Debugger im Gegensatz zu Linux am Ende eines Blockes bereits in den umliegenden Kontext wechselt, und damit das selenium-Objekt vergisst. Sorgt man durch Anfügen eines nichtstuenden Ausdrucks (hier true) dafür, das das debugger-Kommando nicht das letztze im Block ist, ist alles gut:

1
2
3
4
Given /^the debugger is started$/ do
  debugger 
  true
end

Gotcha: Praxiserfahrungen mit Selenium 0

Posted by fwoeck
on Tuesday, April 14

Beim Auffinden von Links nach einem Seitenneuaufbau kommt es in meinem cucumber/webrat/selenium-Stack gelegentlich zu Timingschwierigkeiten. Soweit ich die Sache interpretiere, muss man folgendes beachten:

  1. Zeitangaben werden in cucumber bei Selenium-Aufrufen in Sekunden gesetzt – diese werden automatisch zu Millisekunden konvertiert.
  2. Wenn zuvor kein Seitenaufbau angefordert wurde, wartet die Methode “selenium.wait_for_page_to_load” vergebens.
  3. Falls ein Link auch trotz xpath-Locator nicht gefunden wird, kann ein Pagerefresh helfen.

Beispiele

Hier zwei cucumber steps:

Given I refresh the page
And I follow xpath "//*[@id='p1show']" 
...

und ihre Stepdefinition:

1
2
3
4
5
6
7
8
9
Given /^I refresh the page$/ do
  selenium.refresh
  selenium.wait_for_page_to_load 2
end

When /^I follow xpath "([^\"]*)"$/ do |link|
  selenium.click "xpath=#{link}"
  selenium.wait_for_page_to_load 2
end

Das “wait_for_page_to_load” verringert die Wahrscheinlichkeit von Timingproblemen im Ablauf erheblich.

Selenium fährt kopflos 0

Posted by fwoeck
on Monday, April 06

Hier wird ein Weg gezeigt wie der Browser, den Selenium z.B. während einer cucumber-Session öffnet headless in einem virtuellen Framebuffer ausführbar wird. Auf der Entwicklungsmaschine wäre das nicht unbedingt nötig, aber wenn das Ganze in der CI auf einem Stagingserver läuft, geht’s wohl nicht anders.

Man startet zuerst den Xvfb-Server – hier auf Display Nummer 99 – der das virtuelle Gerät bereitstellt. Dann sorgt man dafür, dass X-Clients wie der Firefox dieses Display auch nutzen und ab geht’s:

Xvfb-Server starten:

Bei mir auf einer Ubuntu 8.04-Maschine in /etc/rc.local:

export PATH=/opt/ruby-enterprise/bin:/usr/local/sbin:
              /usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export DISPLAY=:99

su - -c 'nohup startx -- `which Xvfb` :99 -screen 0 1680x1024x24 &' www-data
su - -c '/opt/ruby-enterprise/bin/thin -C /var/www/integrity/thin.yml 
                -R /var/www/integrity/config.ru start' www-data

(Nebenbei: auf Osx Leopard musste ich explizit /usr/X11R6/bin/Xvfb :99 -screen 0 1680x1024x24 & benutzen, weil sonst der Xvfb aus der port-Installation gestartet wurde und dieser beharrlich die fixed-fonts nicht findet. Das oben beschriebene Verfahren funktioniert allerdings trotzdem nicht auf dem Mac, da der Firefox keine X11-Anwendung ist und daher nicht den Xvfb nutzt.)

thin-Konfiguration:

Ich habe dem Testserver eine eigene IP verpasst und starte einen thin-Server unter einer unprivilegierten Portnummer (8910). Der Versuch, die Konfiguration im Apache mit Passenger/ruby-Enterprise zum Laufen zu bringen schlug leider fehl, da aus unklaren Gründen keine Browserinstanz im virtuellen Framebuffer hochfahren wollte.

Hier also die thin-Konfiguration. Sie wird auf Wunsch von integrity erzeugt. /var/www/integrity/thin.yml:

---
environment: production
chdir: /var/www/integrity
address: 10.10.1.180
port: 8910
pid: /var/www/integrity/thin.pid
rackup: /var/www/integrity/config.ru
log: /var/www/integrity/log/thin.log
max_conns: 1024
timeout: 30
max_persistent_conns: 512
daemonize: true
servers: 1

Bildschirmfotos grabben

Wer dem Unsichtbaren nicht traut, kann zwischendurch einen Screenshot vom Xvfb grabben:

DISPLAY=:99 import -window root /var/www/integrity/public/screenshot.png

Wenn man dies geschickt in seine cucumber-Steps einbaut, gibt’s einen Screenshot bei Beendigung des Tests (und vor allem nach dem letzten Fehler) gratis. Hier wird zu Diagnosezwecken gleich noch ein Datenbankdump angehängt:

1
2
3
4
5
6
After do
  system("import -window root /home/www-data/Desktop/snap_#{Time.now.strftime 
                                    '%y%m%d_%H%M%S'}.png")
  system("mysqldump -upplpool -pp4ssw0rd pplpool_test > /home/www-data/Desktop/
                                    dump_#{Time.now.strftime '%y%m%d_%H%M%S'}.sql")
end

ein neuer rake-Task

Für die CI in integrity (siehe letzter Blogeintrag) habe ich den aufrufenden rake-Task etwas geändert:

1
2
3
4
5
6
7
8
9
10
desc "Execute cucumber features in integrity"
task :integrity do
  require 'ftools'
  File.move(RAILS_ROOT+"/config/database.yml.test", RAILS_ROOT+"/config/database.yml")
  File.move(RAILS_ROOT+"/config/ldap.yml.test", RAILS_ROOT+"/config/ldap.yml")
  ENV["DISPLAY"] = ":99"
  res = `cucumber -p enhanced features/enhanced`.split("\n")
  puts res.join("\n")
  raise if res.grep(/^\d+ failed step/).size != 0
end

geänderter git-Hook für Port 8910

Abschließend habe ich noch den Hook im aufrufenden git-Repo verändert, um der neuen Portnummer Rechnung zu tragen:

wget -q --post-data="" integrity.int.bm.net:8910/marsxpress/builds -O- >/dev/null

Schritt Vier: Selenium-Support 0

Posted by fwoeck
on Saturday, March 28

Wie sieht’s mit Selenium in cucumber und Kumpels aus (webrat kann ja kein JScript/Ajax)? Dies sind die Schritte zur Aktivierung:

Ein paar gems

gem install selenium-client
gem install bmabey-database_cleaner

Der database-cleaner besorgt im Selenium-Modus das Rollback der Datenbank. Im webrat-Modus tut Rails dies selbst durch stornierte Transaktionen.

Firefox-Binary verlinken

In Ubuntu muss ein SymLink auf das aktuelle Firefox-Binary gesetzt werden:

sudo ln -s /usr/lib/firefox-3.0.8/firefox /usr/bin/firefox-bin

Weil er direkt auf die Binaryversion zeigt, zerbricht er leider bei Updates.

Anpassung der cucumber-Umgebung

Da sicher nicht alle Features Ajax/JScript benötigen, ist es pfiffig, zwei getrennte Umgebungen – plain (nur webrat) und enhanced (mit selenium) – zu definieren. Dazu legt man eine entsprechende cucumber.yml in’s Rails-root(!) und schafft die Filestruktur wie unten angegeben:

cucumber.yml:

1
2
3
4
5
default: -r features/support/env.rb -r features/support/plain.rb \
           -r features/step_definitions features/plain

selenium: -r features/support/env.rb -r features/support/enhanced.rb \
           -r features/step_definitions features/enhanced

feature-Tree:

features(devel) > tree
.
|-- enhanced
|   `-- 533386.feature
|-- plain
|   `-- 533386.feature
|-- step_definitions
|   |-- session_steps.rb
|   `-- webrat_steps.rb
|-- support
|   |-- enhanced.rb
|   |-- env.rb
|   |-- paths.rb
|   `-- plain.rb

Hier habe ich zu Testzwecken einfach das gleiche 533386.feature-File für beide Umgebungen benutzt.

Die support/enhanced.rb:

1
2
3
4
5
6
7
8
9
10
11
12
require 'spec/expectations'
require 'selenium'
require 'webrat'
 
Webrat.configure do |config|
  config.mode = :selenium
  config.application_environment = :test
end
 
require 'database_cleaner'
require 'database_cleaner/cucumber'
DatabaseCleaner.strategy = :truncation

Wenn man hier das environment auf “test” setzt, benötigt man keine explizite “selenium”-Railsumgebung mehr.

Die support/env.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + '/../../config/environment')
require 'cucumber/rails/world'
require 'cucumber/formatters/unicode'
Cucumber::Rails.bypass_rescue

require 'webrat'

Webrat.configure do |config|
  config.mode = :rails
end

require 'cucumber/rails/rspec'
require 'webrat/core/matchers'
require 'factory_girl'

Es wird hier vor allem das “use_transactional_fixtures” der normalen Konfiguration entfernt. Dies kommt in die support/plain.rb:


Cucumber::Rails.use_transactional_fixtures

Aufruf der beiden Modi

Auf geht’s:

cucumber

allein ruft nun die plain-Features mit webrat auf und das Kommando

cucumber -p selenium

entsprechend die enhanced-Features mit Selenium. Ein manuelles Starten des selenium-Dienstes ist nicht vorher notwendig.

Wichtig: wie sich gezeigt hat, ist das benutzte webrat-Plugin (Version pre 0.4.3) noch buggy. Dieser Patch beseitigt ein Problem, bei dem einzelne Tests zufällig mit 0ms-Timeouts abbrachen..

Integration mit pickler/Tracker

Die Integration mit dem Pivotal Tracker via pickler funktioniert auch in den Unterordnern – man ruft die Features jetzt nur mit Pfad auf:

pickler pull features/enhanced/539953.feature

Hurra, sehr cool das!

Weblinks

  1. cucumber/setting-up-selenium
  2. webrat/selenium.rb
  3. http://seleniumhq.org/download/
  4. groups.google.com/group/webrat
  5. bmabey/database_cleaner
  6. examples/selenium_webrat
  7. http://gist.github.com/83635