Fake-a-web

Posted by fwoeck
on Saturday, June 13

Das fakeweb gem …

gem install fakeweb

... ist quasi für’s Web, was Mocks und Stubs für die Modelle – in Spec-Läufen kann man fakewebs für einzelne URLs definieren. Bei Net::HTTP-Aufrufen wird dann anstelle der richtigen Netzverbindung der vorher definierte lokale Inhalt ausgegeben.

Ich benutze Factory-Girl als Fixtureersatz und habe die Webdefinitionen fauler Weise mit in deren Datei spec/factories.rb gegeben, weil ich das irgendwie passend fand:

1
2
3
4
5
6
7
8
9
10
11
12
page1 = open("./spec/www_google_de.txt") {|f| f.read }
page2 = open("./spec/www_google_de_en.txt") {|f| f.read }

FakeWeb.register_uri(:get, "http://www.google.de", 
                      [{:string => page1, :status => ["200", "OK"], :times => 4},
                       {:string => page2, :status => ["200", "OK"], :times => 1},
                       {:string => page1, :status => ["200", "OK"], :times => 1}])

FakeWeb.register_uri(:get, "http://www.google.com", :string => 
                      "google.com here!", :status => ["200", "OK"])

FakeWeb.allow_net_connect = false

Hier werden zwei unterschiedliche Adressen definiert, die erste mit dem Inhalt zweier vorher durch

curl -is http://www.google.de/ > ./spec/www_google_de.txt
curl -is http://www.google.de/ncr > ./spec/www_google_de_en.txt

erzeugter Dateien. Indem man der Methode register_uri ein Parameterhash übergibt, kann man eine Rotation des Antwortverhaltens erreichen. Es würde also die ersten vier Male der Inhalt der page1 zurückgegeben, danach der von page2 etc. Nachdem das Array einmal “verbraucht” wurde, reagiert das System immer mit dem letzten Eintrag.

Will man auf eine Anfrage mit einem String antworten, so geht dies natürlich auch (siehe google.com).

Um sicherzugehen, dass während der Tests keine anderen Verbindungen geöffnet werden, kann man diese mit allow_net_connect = false blockieren. Der Verbindungsversuch an eine undefinierte Adresse wirft dann einen Fehler.

Der Model-Code, der schließlich die Netzanrufe auslöst, sieht beim mir so aus:


doc = Nokogiri::HTML(open(self.rwatch_url)).css(self.css_selector)

Das open ist einfach das aus open-uri. (Nokogiri ist übrigens ein hervorragender Ersatz für hpricot.)

Weblinks

github.com/chrisk/fakeweb

autotest-Notifications für rspec/cucumber 0

Posted by fwoeck
on Wednesday, June 10

Die Anzeige von Notifications beim Lauf von autospec ist ein hübsches Extra, aber leider durch die unterschiedliche Einbindung in die verschiedenen Umgebungen (bei mir Ubuntu und OsX) ab und zu mit Fallstricken verbunden. Hier ist für beide Welten geschildert, wie’s bei mir läuft:

Ubuntu

Um unter Gnome oder Xfce4 die Ergebnisse ins Desktop einzublenden, benötigt man die libnotify-Bibliothek:

# apt-get install libnotify-bin libnotify1

Danach sollte dieser Test funktionieren:

$ notify-send Test "Testtest, 23..."

Um mit dem Notify-Dienst Kontakt aufzunehmen, müssen ein paar Hooks in die .autotest-Datei gesetzt werden – leider habe ich es bisher nicht hinbekommen, außer den rspec-Meldungen auch die cucumber-Meldungen anzeigen zu lassen. In der ~/.autotest:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
require 'autotest/timestamp'
 
module Autotest::GnomeNotify
  def self.notify title, msg, img
    system "notify-send '#{title}' '#{msg}' -i #{img} -t 5000"
  end
 
  Autotest.add_hook :ran_command do |at|
    image_root = "~/.autotest_images"
    r = results = [at.results].flatten.join("\n")
    results.gsub!(/\\e\[\d+m/,'')
    output = results.slice(/(\d+)\sexamples?,\s(\d+)\sfailures?/)
    puts output.inspect
    if output
      if $~[2].to_i > 0
        notify "FAIL", "#{output}", "#{image_root}/fail.png"
      else
        notify "Pass", "#{output}", "#{image_root}/pass.png"
      end
    else
      notify "FAIL", "You must have a non-rspec error in your code.\n
          Check the autotest log for details.", "#{image_root}/fail.png"
    end
  end
 
end

Autotest.add_hook :initialize do |at|
  at.add_exception %r%^\./tmp%
  at.add_exception %r%^\./log%
  at.add_exception %r%^\./db%
end

Insbesondere bei der Verwendung von sqlite-DBs, die unter ./db liegen, hat sich das explizite Ignorieren dieses Verzeichnisses als nötig erwiesen: autospec läuft sonst unaufhörlich, da beim Testlauf die Datenbankfiles geändert werden.

Mac

Die Kombination autotest-fsevent und autotest-growl von BitCetera ist sehr zu empfehlen. autotest-fsevent verwendet auf Leopard-Maschinen die FsEvent-Mechanik und entlastet so die CPU dadurch, dass nicht unaufhörlich nach Fileänderungen gesucht werden muss.

sudo gem install autotest-fsevent
sudo gem install autotest-growl
~ > cat .autotest 
require 'autotest/growl'
# fsevent als letztes(!):
require 'autotest/fsevent'

Tip: ein eventuell zusätzlich vorhandenes ZenTest < 4.1.1 sollte man explizit deinstallieren. Dies killt i.d.R. auch die Binaries einer neueren Version, so dass danach in jedem Falle ein erneutes

gem install ZenTest

fällig ist.

Teststart

Bevor’s losgeht, sollte man noch folgende zwei Variablen in die Systemumgebung bringen (z.B. in der .bashrc):

AUTOFEATURE=true
RSPEC=true

Auf beiden Systemen wird der kontinuierliche jetzt Test so gestartet:

RAILS_ENV=test autospec 2>/dev/null

Weblinks

  1. www.bitcetera.com/.../mac-friendly-autotest/
  2. www.bitcetera.com/.../autotest-fsevent
  3. github.com/svoop/autotest-growl
  4. github.com/svoop/autotest-fsevent

integrity 0.1.10 + rack 1.0.0 + thin 1.2.2 + sinatra 0.9.1.3 0

Posted by fwoeck
on Friday, May 29

Ich hoffe, dass sich die Aktualität dieses Artikels bald erledigt: kurz gefasst, die aktuelle gem-Version von Thin basiert auf rack 1.0.0, die aktuelle integrity-Version auf sinatra 1.9.1.1, welches selbst rack 0.9.x benötigt. Wenn man rack oder Thin oder beide updated, entstehen ein paar Probleme.

Dies ist – soweit ich es jetzt noch zusammenbekomme – wie ich auf meinem Ubuntu-Testserver eine (vermutlich) lauffähige Version zusammengefrickelt habe:

Eine sinatra-Version von SR (s.u.) clonen

Von http://github.com/sr/sinatra/tree/layout-local das git://github.com/sr/sinatra.git clonen und den branch layout-local auschecken:

> git checkout -b layout-local
> vi lib/sinatra/base.rb

dort musste ich für mein REE-1.8.6… die Zeile #46:

inject(0) { |len, part| len + part.bytesize }.to_s
gegen
inject(0) { |len, part| len + part.length }.to_s
ersetzen. Das ist natürlich nicht das Gleiche …

Anschließend noch ein

> rake install

Die letzte integrity-Version clonen

Von http://github.com/integrity/integrity/tree/master den git://github.com/integrity/integrity.git clonen:

> git clone git://github.com/integrity/integrity.git
> cd integrity
> vi integrity.gemspec  # 0.9.1.1 > 0.9.1.3
> gem uninstall integrity
> gem build integrity.gemspec 
> gem install integrity-0.1.10.gem

In der .gemspec muss die Abhängigkeit auf die Version 0.9.1.3 gesetzt werden, damit integrity auch die aktuelle sinatra-Version aufruft.

Weblinks

  1. irclogger.com/integrity/2009-05-19
  2. github.com/integrity/integrity/tree/master
  3. github.com/sr/sinatra/tree/layout-local
  4. http://integrityapp.com/

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

Update cucumber 0.2 > 0.3 0

Posted by fwoeck
on Saturday, April 18

Das aktuelle cucumber-Update auf die Version 0.3 führt auf Anhieb zu einer hässlichen Fehlermeldung, weil die Art und Weise, wie das world-Objekt verwaltet wird sich geändert hat. In dieses schreibt man für gewöhnlich seine Helfermethoden, um den Objectspace nicht zu vermüllen.

Eine kleine Modifikation dieser Methoden (hier in /features/support/paths.rb) tut’s:

pplpool(devel) > git diff HEAD^
diff --git a/features/support/paths.rb b/features/support/paths.rb
index 925e67a..218c5a6 100644
--- a/features/support/paths.rb
+++ b/features/support/paths.rb
@@ -13,7 +13,5 @@ module NavigationHelpers
   end
 end

-World do |world|
-  world.extend NavigationHelpers
-  world
-end
+World(NavigationHelpers)
+

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.

Caching-Fehler während eines Testlaufs diagnostizieren 0

Posted by fwoeck
on Tuesday, April 14

In Projekten, die z.B. memcached als Cachingserver für SQL-Anfragen und Seitenfragmente einsetzen, kann das Cachemanagement selbst eine erhebliche Fehlerquelle sein.

Da sich die Inhalte des Cachingservers nicht ohne weiteres zwischen zwei Testabschnitten löschen lassen, ist es wohl am sichersten, den Server in jedem Scenario neu zu starten. Hierzu spricht man am besten eine separate Instanz auf einem eigenen Port an, z.B. 11213 anstelle 11212.

Cucumber

Ich benutze hier cucmber und Selenium als Testplattform. Um den Caching-Dienst für den Testlauf zu starten, trage ich ein entsprechendes Statement als Before-do-Schritt ein und anschließend einen kill-Befehl als After-Do-Schritt:

1
2
3
4
5
6
7
Before do
  system("memcached -l 127.0.0.1 -p 11213 -d")
end

After do
  system("killall memcached 2>/dev/null")
end

Ein eventuell regulär laufender memcached sollte unter einem anderen Useraccount laufen als der cucumber-Task, um zu vermeiden, dass er vom kill kurzerhand mitgetilgt wird.

Rails-Konfiguration

Damit der Server letztlich auch benutzt wird, muss er in der config/environments/test.rb aktiviert sein:

config.cache_store = :mem_cache_store, "localhost:11213"

Auch das Caching sollte aktiv sein – sonst ist es nicht das echte Leben:

config.cache_classes = true
config.action_controller.perform_caching = true
config.action_view.cache_template_loading = true

ein Testlauf

Jetzt kann man den Testlauf starten und währenddessen die aktive Cachingserverinstanz prüfen:

~ > memstat --servers=127.0.0.1:11213
Listing 1 Server

Server: 127.0.0.1 (11213)
         pid: 13500
         uptime: 4830
         time: 1239700285
         ...

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

"integrity" leistet kontinuierliche Integrationsarbeit 0

Posted by fwoeck
on Sunday, April 05

integrity führt automatisch oder manuell Test-Builds von Softwareprojekten durch – in diesem Fall von meinem aktuellen Railsprojekt. Das integrity-Frontend läuft als Thin- oder passenger-Website auf einem eigenen Server und erwartet zur Konfiguration unter anderem die git-Repoadressen der verwalteten Projekte.

Basisinstallation

# gem install foca-integrity
# gem install foca-integrity-email
# gem install ph-integrity-jabber
# gem install do_sqlite3
# gem install do_mysql

$ integrity install --passenger /var/www/integrity

Konfiguration

Die Datei /var/www/integrity/config.ru muss angepasst werden, falls Benachrichtigungen versendet werden sollen:

require "notifier/email" 
require "notifier/jabber"

und ein virtueller apache-Host muss eingerichtet werden:

<VirtualHost *:80>
  DocumentRoot /var/www/integrity/public
  ServerName integrity.int.bm.net
<Directory "/var/www/integrity/public">
allow from all
Options +Indexes
</Directory>
  ServerAlias integrity
</VirtualHost>

Die Datei config.yml enthält u.a. Datenbankeinstellungen:

:base_uri: http://integrity.int.bm.net
:database_uri: mysql://integrity:p4ssw0rd@localhost/integrity

DB-Einrichtung

Die Basisdatenbank für alle Projekte zusammen:

# mysqladmin -uroot -p create integrity
# mysql -uroot -p integrity

mysql> grant all on integrity.* to 'integrity'@'localhost' identified by 'p4ssw0rd';
mysql> flush privileges;
mysql> \q

DB-Migration

Ob es meine Ignoranz oder ein Bug war – ich musste die Migrationen für die Datenbank von integrity manuell einspielen:

CREATE TABLE `integrity_projects` (`id` INT(11) PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL, `permalink` VARCHAR(50), `uri` VARCHAR(255) NOT NULL DEFAULT '', `branch` VARCHAR(50) NOT NULL DEFAULT 'master', `command` VARCHAR(50) NOT NULL DEFAULT 'rake', `public` tinyint(1) default '1', `building` tinyint(1) default '0', `created_at` DATETIME, `updated_at` DATETIME, `build_id` INT(11), `notifier_id` INT(11))

CREATE TABLE `integrity_notifiers` (`id` INT(11) PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL, `config` TEXT NOT NULL, `enabled` tinyint(1) default '1', `project_id` INT(11))

CREATE TABLE `integrity_commits` (`id` INT(11) PRIMARY KEY AUTO_INCREMENT, `identifier` VARCHAR(50) NOT NULL default '', `message` VARCHAR(255) NOT NULL default '', `author` VARCHAR(50) NOT NULL default '', `committed_at` DATETIME NOT NULL, `created_at` DATETIME, `updated_at` DATETIME, `project_id` INT(11))

CREATE TABLE `integrity_builds` (`id` INT(11) PRIMARY KEY AUTO_INCREMENT, `started_at` DATETIME, `completed_at` DATETIME, `successful` tinyint(1) default null, `output` TEXT NOT NULL, `created_at` DATETIME, `updated_at` DATETIME, `commit_id` INT(11))

Konfiguration eines Projekts

Die folgenden Files werden im jeweiligen Projektrepo erzeugt und revisioniert, damit integrity sie bei einem Durchlauf aufrufen kann. Es erwartet einen rake-Task, der für das Build gestartet wird. Hier ist ein Beispiel:

lib/tasks/integrity.rake:

1
2
3
4
5
6
7
8
9
desc "Execute plain 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")
  res = `cucumber -p plain features/plain`.split("\n")
  puts res.join("\n")
  raise if res.grep(/^\d+ failed step/).size != 0
end

An dieser Stelle kann schon einmal geprüft werden, ob der Task separat durchläuft – Achtung: er überschreibt die originalen YML-Dateien. Auf dem Entwicklungsserver:

rake integrity

Die Verarbeitung der Ausgabe nach res ist nötig, um den rake-Task scheitern zu lassen, falls ein Schritt fehlschlägt und dennoch eine sinnvolle Ausgabe im Buildlog zu sehen.

database.yml.test und ldap.yml.test

Meist werden die YML-Konfigurationsfiles nicht revisioniert. In diesem Fall musste ich zwei .test-Files erzeugen und einchecken, damit der rake-Task des Integrationsservers sie vor dem Test umkopieren kann (s.o.).

Umgebung für ruby-Enterprise

Da cucumber hier unter der apache2-Umgebung ausgeführt wird, muss diesem der rechte Pfad mitgegeben werden, d.h. in Ubuntu muss in die /etc/environment und die /etc/apache2/envvars ein /opt/ruby-enterprise/bin eingefügt werden.

Abschließende Einrichtung und Start

Nach der Vorarbeit kann nun das Frontend aufgerufen und ein erstes Testprojekt eingerichtet werden:

http://integrity.int.bm.net

Dort gibt man unter anderem Projektname und Repo an und führt den ersten Build manuell durch:

Autotests mit post-receive-Hooks

Es fehlt noch die Konfiguration von Auto-Tests via git-Postcommithooks. Am besten benutzt man das Stagingrepo dafür:

/home/git/repositories/marsxpress.git/hooks/post-receive:

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

Damit das Script nach einem Commit aufgerufen werden kann, braucht es Ausführrechte:

chmod a+x post-receive

Benachrichtigungen mit Mail und Jabber

Hat man die enstprechenden Bibliotheken in der config.ru required (notifier/email, ...), so lässt sich die jeweilige Form im Webfrontend konfigurieren. Die Jabber-Variante habe ich beim ersten Versuch jedoch nicht zum Laufen gebracht.

Um den Mailer ohne Auth – also im Relay – zum Arbeiten zu bewegen, musste ich in der gem-Datei .../foca-integrity-email-1.0.1/lib/notifier/email.rb die Einträge für user, pass und auth entfernen. Hm.

Weblinks

  1. locomotivation.com/integrity
  2. integrityapp.com
  3. github.com/foca/integrity

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

Der dritte Schritt: Factory-Girl 0

Posted by fwoeck
on Saturday, March 28

Die ersten Integrationstests im marsXPress sind in der Test-DB gelaufen, in die zuvor Datensätze auf konventionellem Weg geschrieben wurden (User.create…) – dies ist jetzt gefixtured! Hier wird gezeigt, wie die User-Stories und deren Schrittdefinitionen gestrickt sind und wie die benutzen Modelle mit Factory-Girl definiert sind.

Factories

Hier habe ich mal die Gerippe der Modelle User, Roll und Member eingesetzt. Die AR-Definitionen sind so gesetzt, dass

User ... has_many :rolls, :through => :members

Member hat nur zwei Spalten user_id und roll_id. Die Factory-Definitionen liegen zentral in spec/factories.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
require 'factory_girl'
 
Factory.define :user do |u|
  u.email 'me@bm.net' 
  u.password 'p4ssw0rd'
  u.password_confirmation 'p4ssw0rd'
end

Factory.define :roll do |u|
  u.name "System Services"
end

Factory.define :member do |u|
  u.association :user
  u.association :roll
end

Mit der Methode association zeigt man an, dass es sich um einen Proxy handelt. Allgemein gelten die Parameter als Defaults und können bei der Instanziierung überschrieben werden.

env.rb

In der env.rb sollte man entgegen der Voreinstellung die Test-Umgebung erzwingen, um Reibungsverluste zu vermeiden:

ENV["RAILS_ENV"] = "test"

Die Feature-Datei

Die Cucumber-Features sind weitergeführt, wie im letzten Artikel diesbzgl. beschrieben:

1
2
3
4
5
6
7
8
9
10
11
12
13
Feature: Users can log in and out
  In order to access the website
  As a user
  I can login to and logout from my account

  Scenario: admin-user logs in to the website
    Given I am not yet logged in
    When I visit "/"
    And I enter the admin's username and password
    And I click on "anmelden"
    Then I should see "Du hast Dich angemeldet."
    And I should see "Abmelden (Admin)"
...

Die Step-Definitionen

Hier wird’s spannender:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Before do
  Given "the admin-user account exists"
end

Given /^I am not yet logged in$/ do
  visit new_session_path
  response.should contain("Anmelden")
end

When /^I visit "([^\"]*)"$/ do |arg1|
  visit arg1
end

When /^I click on "([^\"]*)"$/ do |arg1|
  click_button arg1
end

When /^I enter the admin's username and password$/ do
  fill_in "login", :with => "admin"
  fill_in "password", :with => "p4ssw0rd"
end

Given /^the admin\-user account exists$/ do
  unless User.find_by_login("admin")
    roll = Factory.create(:roll, :name => 'Admins', :id => 1)
    user = Factory.create(:user, :ldap => false, :login => 'admin', 
               :name => 'Admin User', :roll_ids => [roll.id], :id => 1)
    member = Factory.create(:member, :roll => roll, :user => user)
    user.activate!
  end
end

...

Wie man sieht, kann man die ids der Datensätze festlegen falls erforderlich, dies geht mit AR nicht so direkt ( :id => 1 ).

Cucumber-Lauf

Und schließlich kommt der Lauf:

pplpool(devel) > cucumber features/533386.feature:7

# http://www.pivotaltracker.com/story/show/533386
Feature: Users can log in and out
  In order to access the website
  As a user
  I can login to and logout from my account

  Scenario: admin-user logs in to the website
    Given I am not yet logged in
    When I visit "/" 
    And I enter the admin's username and password
    And I click on "anmelden" 
    Then I should see "Du hast Dich angemeldet." 
    And I should see "Abmelden (Admin)" 

1 scenario
6 passed steps

Weblinks

  1. webjam, Factory-Beispiele mit Cucumber und rSpec
  2. Video-Trailer

Der zweite Schritt: Integrationstests mit cucumber 0

Posted by fwoeck
on Friday, March 27

Der nächste witchtige Schritt ist getan: die ersten Integrationstests mit cucumber und webrat sind für das laufende Projekt geschrieben:

pplpool(bdd) > RAILS_ENV="test" cucumber features/533386.feature

# http://www.pivotaltracker.com/story/show/533386
Feature: Users can login
  In order to get access to the website
  As a user
  I first must login

  Scenario: Admin-User logs in to the website
    Given I am not yet logged in
    And the admin-user account exists 
    When I visit "/" 
    And I enter the admin's username and password
    And I click on "anmelden" 
    Then I should see "Du hast Dich angemeldet." 
    And I should see "Abmelden (Admin)" 

  Scenario: User logs in to the website
    Given I am not yet logged in
    And a user account exists
    When I visit "/" 
    And I enter a valid username and password
    And I click on "anmelden" 
    Then I should see "Du hast Dich angemeldet." 
    And I should see "Abmelden (You)" 
2 scenarios
14 passed steps

Hurra!

Hurra!

Mir gefällt noch nicht, dass es diese beiden

And a ... user account exists

gibt, die eigentlich keine sichtbaren Verhalten der Views beschreiben – je nun, im Moment fällt mir dazu noch nichts besseres ein.

Zaghafte agile Schritte 0

Posted by fwoeck
on Thursday, March 26

Während meiner Annäherungen an das BDD, die hier noch weiter undokumentiert sind, kommt mir ein gem unter, das User-Stories eines laufenden Tracker-Projekts mit cucumber-Features eines Rails-Projekts synchronisiert.

Es heisst pickler und installiert sich wie folgt:

sudo gem install tpope-pickler
echo "api_token: <das apitoken hier>" > ~/.tracker.yml
cd /pfad/zum/rails-root/
echo "project_id: 11773" > ./features/tracker.yml
echo "ssl: false" >> ./features/tracker.yml

Stories verwalten

Damit kann man die Stories durchsuchen, starten, etc.:

> pickler search
533386 :/ 1 Users can login

> pickler start 533386

> pickler comment 533386 "give a nifty comment"

Wie’s aussieht, werden die Änderungen in der Website aktiv gepusht, sodass angemeldete User diese sofort wahrnehmen – sehr hübsch!

Features erzeugen

Wenn ich aus den Stories Cucumber-Features erzeugen möchte, “pull”e ich die zugehörige Nummer und es wird ein Template gleichen Namens erstellt:

> pickler pull 533386

> cat features/533386.feature 
# http://www.pivotaltracker.com/story/show/533386
Feature: Users can login
  User can (must) login to the website to be able to do things

So sieht man auch, wozu die Description in den Stories gut ist – sie gibt die sprachliche Feature-Umschreibung. Eine bessere Variation davon wäre vielleicht:

Feature: Users can login
  In order to to do things on the website
  As a frontend user
  I want to be able to log in

Weblinks

  1. pivotaltracker.com
  2. tpope/pickler