Ejecutando Pruebas de Aceptación con Story Runner de RSpec
He estado utilizando rspec ya desde hace tiempo (gracias a chrisstrum) y me mola mazo :) Te sientes muy cómodo y es muy intuitivo y incluso estoy aprendiendo como y cuando utilizar mocks/stubs (aunque todavía tengo algunos fixtures por ahí) Tenia ganas de aprender sobre el nuevo story runner y mientras estaba googleando me encontré con un post de Kerry Buckley en el cual explica un poco como montar story runner para que funcione y de paso describe como se lo monto para que story runner ejecutara las pruebas de aceptación de Selenium.
Ahora os explico un poco lo que Story Runner es…
Básicamente te permite escribir especificaciones en un fichero de texto normal y corriente y ademas en lenguaje natural (Es mas natural si los haces en ingles :). Básicamente escribes una historia (parafraseando a Dan North “una descripción de un requerimiento y su beneficio, y un conjunto de criterios con los cuales todos estamos de acuerdo que está hecho”.)
Para cada historia puedes escribir varios diferentes escenarios (imagine el requerimiento en diferentes situaciones) y por cada escenario escribes un conjunto de criterios que determinan como el escenario puede satisfacerse con éxito.
e.g.1 2 3 4 5 6 7 8 9 10 11 12 13 |
Story: UI
As a developer #
I want to go to the uimockups page # Description of intent
So that I can implement the mockup #
Scenario: Going to the /uimockups page when not logged in <= Scenario Description
Given an anonymous user #
When the user goes to /uimockups #
Then the document title should be 'personal' # criteria, actions & expectations
And the page should contain the text 'done by Webtypes' #
And the page should have a field named 'strip-search-input' #
And the page should have a form named 'strip-search' #
|
Dado un fichero de texto como este, luego escribes un pequeño script de ruby (ver /stories/stories/project.rb mas abajo) que coge el texto, lo parsea buscando palabras clave. Cada Given, When and Then es un Step (peldaño). Los Ands son el mismo tipo de ‘step’ como el anterior step.
Ejecutado tal cual, te devolverá la misma historia pero con la diferencia que cada uno de las lineas debajo de Scenario estarán marcados con “pending” que básicamente significa que la historia todavía no ha sido implementado. e.g.
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 |
saimon@artemis~/dev/projects/myrailsapp$ ruby stories/stories/project.rb
(in /Users/saimon/dev/projects/myrailsapp)
Running 1 scenarios
Story: UI
As a developer
I want to go to the ui page
So that I can see the mockup
Scenario: Going to the /ui page when not logged in
Given an anonymous user (PENDING)
When the user goes to /ui (PENDING)
Then the document title should be 'personal' (PENDING)
And the page should contain the text 'done by Webtypes' (PENDING)
And the page should have a field named 'strip-search-input' (PENDING)
And the page should have a form named 'strip-search' (PENDING)
1 scenarios: 0 succeeded, 0 failed, 1 pending
Pending Steps:
1) UI (Going to the /ui page when not logged in): Unimplemented step: an anonymous user
|
Para que la historia aprueba, necesitas implementar cada uno de los Steps en ruby. i.e. Aquí hay une ejemplo de la implementación del 2º step:
- “When the user goes to /ui (PENDING)”
1 2 3 4 5 6 |
steps_for(:project) do When "the user goes to $path" do |path| get path end end |
Como puedes ver, está básicamente parseando la linea para una palabra clave que indica un step (en este caso ‘When’), y entonces coge el resto de la linea y intenta emparejar con cualquier de los When steps que conoce. Incluso, da un paso mas y te permite incluir variables para que puedas extraer datos directamente de la linea de la historia ($path en este caso).
Una vez emparejado, acaba ejecutando:
1 2 |
get /uimockups
|
Lo cool de esto es que una vez has implementado un step, se puede reutilizar cada vez que se encuentra en una historia. Puedes incluso tener un conjunto de steps ya implementados para reutilizarlos en múltiples historias. Cambien, podridas crear una librería de steps para ser utilizados en otras aplicaciones. Así que aunque al principio, tienes un gasto de tiempo mas, es una inversión que con tiempo da fruto.
Estaba en la reunión del grupo de Ruby/Rails de BCN anoche y unos de los presentes expresó un duda sobre la fragilidad del sintaxis. De hecho, no hay problema porque story runner marcará como pendiente cualquier linea que no ha podido emparejar contra los steps que conoce, así que puedes identificar con facilidad un problema de sintaxis. Y si algo que ya ha reconocido se lanza una excepción, nos dará el stack trace apropiado indicando el step que lo causo.
Después de haber visto el screen-cast de Pat Maddox me ha convencido que utilizar el story runner es una buena manera de empezar de escribir tus especificaciones. Puedes simplemente empezar por escribir una historia que describe una funcionalidad y entonces mientras entras mas a fondo en la historia vas implementando los steps. Por el camino, encuentras que necesitas implementar controladores, modelos, helpers and vistas y antes de hacerlo escribes la especificación apropiada (solo lo bastante para tener la funcionalidad en la historia) que a su vez mueve la implementación del objeto.
Finalmente, llego a la razón principal que escribí este post.
Tengo ganas de poder hacer mis pruebas de integración con el story runner y también, de vez en cuando, hacer pruebas de aceptación en el mismo navegador y como también tenia ganas de jugar con FireWatir & SafariWatir decidí adaptar el código de Kerry un poquito.
Pero añadí otro requerimiento. Lo que realmente querría era una integración transparente entre pruebas de integración utilizando rspec mismo y pruebas de aceptación en el navegador y entonces ejecutara los mismos escenarios o incluso poder mezclar los como querría.
Después de un rato llegué al siguiente montaje (bastante similar al original de Kerry):
Estructura de directorios:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
+-- lib | +-- tasks/ | +-- acceptance.rake +-- stories/ | +-- all.rb | +-- helper.rb | +-- steps/ | | +-- project.rb | | +-- watir.rb | +-- stories/ | | +-- project.rb | | +-- project.txt |
Nota: Como en el articulo de Kerry he subdividido el directorio principal “stories” a los subdirectorios “stories” y “steps”. No hace falta si no tienes tantas historias para escribir pero a mi me gusta que esté todo organizado.
/stories/all.rb1 2 3 4 5 6 |
dir = File.dirname(__FILE__) require "#{dir}/helper" Dir[File.expand_path("#{dir}/stories/**/*.rb")].uniq.each do |file| require file end |
1 2 3 4 5 6 7 8 9 10 11 12 |
Story: UI As a developer I want to go to the ui page So that I can see the mockup Scenario: Going to the /ui page when not logged in When the user goes to /ui Then the document title should be 'personal' And the page should contain the text 'done by Webtypes' And the page should have a field named 'strip-search-input' And the page should have a form named 'strip-search' |
1 2 3 4 |
#Ejecutarme con: [BROWSER=firefox|safari|ie] ruby stories/stories/project.rb require File.join(File.dirname(__FILE__), "../helper") run_story_with_steps_for (browser ? [:watir_project, :project] : [:project]) |
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 |
steps_for(:project) do Given "a test user" do User.delete_all User.create!(:name => 'test', :openid_url => 'http://dummy.openid/', :email => 'test@example.com') end When "the user goes to $path" do |path| get path end Then "the document title should be '$title'" do |title| response.should have_tag('title', title) end Then "the page should contain the text '$text'" do |text| response.should have_text(/#{text}/) end Then "the page should have a field named '$field'" do |field| response.should have_tag("input[type=text][id=?]", field) end Then "the page should have a form named '$form'" do |form| response.should have_tag("form[id=?]", form) end Then "the page should have a submit button named '$name', with the label '$label'" do |name, label| response.should have_tag("input[type=submit][id=?][value=?]", name,label) end end |
/stories/steps/watir_project.rb
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 34 35 |
steps_for(:watir_project) do When "the user goes to $path" do |path| browser.goto "http://localhost#{path}" end When "the user types '$text' into the $field field" do |text, field| browser.text_field(:name,field).set(text) end When "the user clicks the $button button" do |button| browser.button(:value, button).click end Then "the document title should be '$title'" do |title| browser.title.should == title end Then "the page should contain the text '$text'" do |text| browser.text.include?(text).should be_true end Then "the page should have a field named '$field'" do |field| (browser.text_field(:name, field).exists? || browser.text_field(:id, field).exists?).should be_true end Then "the page should have a form named '$form'" do |form| (browser.form(:name, form).exists? || browser.form(:id, form).exists?).should be_true end Then "the page should have a submit button named '$name', with the label '$label'" do |name, label| tf = (browser.text_field(:name, field) || browser.text_field(:id, field)).exists?().should be_true tf.value.should == label end end |
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
ENV["RAILS_ENV"] = "test" require File.expand_path(File.dirname(__FILE__) + "/../config/environment") require 'spec/rails/story_adapter' # watir gem require 'firewatir' if ENV['BROWSER'] && ENV['BROWSER'] == 'firefox' require 'safariwatir' if ENV['BROWSER'] && ENV['BROWSER'] == 'safari' def start_ff FireWatir::Firefox.new end def start_safari safari = Watir::Safari.new end #Require steps in steps dir Dir[File.dirname(__FILE__) + "/steps/*.rb"].uniq.each { |file| require file } #Require appropriate watir browser object if !$ff && ENV['BROWSER'] == 'firefox' $ff = start_ff_with_logger end if !$sf && ENV['BROWSER'] == 'safari' $sf = start_safari_with_logger end #Choose which browser to use in steps def browser $ff || $sf end def run_story_with_steps_for *steps with_steps_for *(steps.flatten) do # Pull the filename of the caller out of the stack. Must be a better way. run caller[3].sub(/\.rb:.*/, '.txt'), :type => RailsStory end end # By default, RSpec adds an ActiveRecordSafetyListener to the story runner. # This rolls back database changes between scenarios, which is great if your calling your code directly, # but obviously means that if you write to the database, the server that Selenium's talking to can't see them. There's probably a cleaner way of disabling it. class Spec::Story::Runner::ScenarioRunner def initialize @listeners = [] end end module ::ActionController #:nodoc: module TestProcess # Work around Rails ticket http://dev.rubyonrails.org/ticket/1937 # Helps to remove annoying html parser warnings def html_document @html_document ||= HTML::Document.new(@response.body, true, true) end end end |
Una vez montado todo eso puedes ejecutar:
1 2 3 |
saimon@artemis~/dev/projects/myrailsapp$ ruby stories/stories/project.rb |
para ejecutar la historia “project” utlizando rspec basico. Da el siguiente output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
Running 1 scenarios
Story: UI
As a developer
I want to go to the ui page
So that I can see the mockup
Scenario: Going to the /ui page when not logged in
Given an anonymous user
When the user goes to /ui
Then the document title should be 'personal'
And the page should contain the text 'done by Webtypes'
And the page should have a field named 'strip-search-input'
And the page should have a form named 'strip-search'
1 scenarios: 1 succeeded, 0 failed, 0 pending
|
Genial! Ahora puedes coger el fichero project.txt y mandaselo a un cliente, un compañero desarrollador, una lista de correos de un proyecto etc…
Pero, vamos a dar el paso siguiente y ejecutar esa misma historia contra Firefox (ir a FireWatir y sigue las instrucciones. Son fáciles.)
Arranca firefox con -jssh1 2 3 4 5 6 7 8 |
saimon@artemis~/dev/projects/myrailsapp$ /Applications/Firefox.app/Contents/MacOS/firefox -jssh ejecuta la historia con el variable de entorno BROWSER: saimon@artemis~/dev/projects/myrailsapp$ BROWSER=firefox ruby stories/stories/project.rb |
y observa como FF es dirigido como si por magia a ejecutar todo lo que hay en los escenarios de tu historia(s). Cuando acabe de da el mismísimo output que antes pero esta vez como si lo hubieses hecho tu mismo en el navegador.
Lo que también hice fue escribir unos comandos de rake para simplificar ejecutar todos tus historias con o sin el navegador.
Añade este fichero:
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
desc "Run the acceptance tests, starting/stopping the test server." task :acceptance_with_browser => ['acceptance:server:start'] do begin Rake::Task['acceptance:run'].invoke ensure Rake::Task['acceptance:server:stop'].invoke end end %w(firefox safari).each do |browser| Object.class_eval <<-EOS desc "Run the acceptance tests using the #{browser} browser." task :acceptance_with_#{browser} do $browser = '#{browser}' Rake::Task['acceptance_with_browser'].invoke end EOS end namespace :acceptance do desc "Run the acceptance tests." task :run do system "#{$browser ? "BROWSER='#{$browser}' " : ''}ruby stories/all.rb" end namespace :server do desc "Start the mongrel server" task :start do system 'script/server -e test -d' sleep 5 end desc "Stop the mongrel server" task :stop do if File.exist? MONGREL_SERVER_PID_FILE pid = File.read(MONGREL_SERVER_PID_FILE).to_i Process.kill 'TERM', pid FileUtils.rm MONGREL_SERVER_PID_FILE else puts "#{MONGREL_SERVER_PID_FILE} not found" end end end end MONGREL_SERVER_PID_FILE = 'tmp/pids/mongrel.pid' |
Y entonces:
1 2 3 4 5 6 7 8 9 10 11 |
saimon@artemis~/dev/projects/myrailsapp$ rake acceptance:run o rake acceptance_with_firefox o rake acceptance_with_safari |
y ya está… :)
He disfrutado mucho montado todo esto y aunque solo me planteo escribir especificaciones para el navegador muy de vez en cuando, mola tener la opción y por su valor geek.
Disfruta especificando… :)
un comentario
Saltar al formulario de comentarios | comentarios rss [?]