Jump to content

The ultimate community for Ruby on Rails developers.


Photo

Better integration testing with RSpec

testing rspec poltergeist integration

  • Please log in to reply
No replies to this topic

#1 chunky_bacon

chunky_bacon

    Passenger

  • Members
  • 1 posts

Posted 12 October 2013 - 03:53 PM

I was always a big fan of cucumber features - they are easy to read and
understand moreover writing them is simple almost for anybody (not
the implementation though). However they have it's own downfalls - managing
steps ia a hell, you often end up having two similar ones. Regular
expressions help a little but the problem is still there.
 
So this article is my attempt to make rspec integration tests as
beautiful as cucumber and super easy to maintain, improve and re-use existing.
 
I was greatly inspired by Ryan Bates's rails casts and one more
blog post that I'm unable to find anymore.
 
So let's dive right into code and let it tell the story:
 
## basic_page.rb [ruby_on_rails]
class BasicPage
  include Capybara::RSpecMatchers
  attr_reader :session

  # page url
  URL = "/"
  # default CSS wrapper for all nodes
  CSS_WRAPPER = ''

  def url
    self.class::URL
  end

  #initialize with Capybara session
  def initialize(session)
    @session = session
  end

  def visit
    session.visit url
  end

  # saves screenshot of the page
  # works only for poltergeist
  def current_state( filename = self.class.to_s.underscore )
    return true if !Rails.env.test?
    begin
      self.session.driver.render("#{filename}.png", :full => true)
    rescue
      self.session.save_and_open_page
    end
  end

  # starts debugging
  def start_browser
    return false if !Rails.env.test?
    begin
      self.session.driver.debug
    rescue
      false
    end
  end

protected

  # a little of magic :)
  # will define methods to use/validate nodes
  # on the page
  def self.has_node( method_name, selector, default_selector = :css )
    if :css == default_selector
      # asccessor
      define_method( method_name ) do
        node_selector = self.class::CSS_WRAPPER + ' ' + selector
        self.session.find( default_selector, node_selector.strip )
      end
      # validators
      define_method( :"#{method_name}_present?" ) do
        node_selector = self.class::CSS_WRAPPER + ' ' + selector
        self.session.should have_css( node_selector.strip )
      end
      define_method( :"#{method_name}_missing?" ) do
        node_selector = self.class::CSS_WRAPPER + ' ' + selector
        self.session.should_not have_css( node_selector.strip )
      end
    else
      # accessor
      define_method( method_name ) do
        self.session.find( default_selector, selector )
      end
      # validators
      define_method( :"#{method_name}_present?" ) do
        self.session.should have_selector( default_selector, selector )
      end
      define_method( :"#{method_name}_missing?" ) do
        self.session.should_not have_selector( default_selector, selector )
      end
    end
  end
end
It may look big and scarry at first however you'll love it soon :).
The main part of it is the "has_node" method that creates three
methods to access, verify existence nad verify presence of the 
node on the page using RSpec. We'll go over the rest of 
constants/methods in the next example:
 
## my_page.rb [ruby_on_rails]
require "basic_page" 

class MyPage < BasicPage

  URL = '/my/url'
  CSS_WRAPPER = '#my'
  #
  # Attributes ( elements )
  #

  #
  # Methods ( actions )
  #

  #
  # Validators ( checks)
  #

end
So first we have two constants - URL and CSS_WRAPPER. The first
one used inside BasicPage#visit method to visit the page and
the second one is used inside BasicPage#has_node method when selector
is css (we'll take a look at this method later). Except for those constants
it's divided into three sections where we'll define different types
of methods. Let's write a simple test now that doesn't do anything
except for visiting this page and rendering preview.
 
## my_page_test.rb
require 'spec_helper'

feature 'Visitor works with my page' do
  before do
    login
    @my_page = MyPage.new( Capybara.current_session )
    @my_page.visit
  end

  scenario 'preview page' do
    @my_page.current_state
  end
end
This is usual setup - login and visit page, as a result you'll have a preview
of your page. Now let's test if the page was rendered  properly and has expected
HTML nodes.
## my_page.rb [ruby_on_rails]
require "basic_page" 

class MyPage < BasicPage

  URL = '/my/url'
  CSS_WRAPPER = '#my'
  #
  # Attributes ( elements )
  #
  
  has_node :my_button, '#button'
  has_node :my_image,  '//img[contains(@src,"edit.png")]', :xpath
  has_node :my_link,   { :text => 'Save' }, 'a.my_css'
  
  #
  # Methods ( actions )
  #

  #
  # Validators ( checks)
  #

  def validate_main_nodes
    my_button_present?
    my_image_present?
    my_link_present?
  end
end

First we declare that there are three nodes on the page. 

The BasicPage::has_node method internaly uses have_css if no 
selector declared and have_selector othervise. First two 
samples should be clear - find nodes using css or xpath. 
The last one is different - it looks for a link with particular 
text however since params are directly passed to the have_selector 
method we can use any format that the have_selector can understand.
The first node we declared also will not search for
the '#save_process_cancel_button' but for '#my #save_process_cancel_button'
because of CSS_WRAPPER value. 
 
The validation part is based on nodes we defined - the BasicPage::has_node method
defines these validators for you.
 
Now let's use it onside our spec
 
## my_page_test.rb
feature 'Visitor works with my page' do
  before do
    login
    @my_page = MyPage.new( Capybara.current_session )
    @my_page.visit
  end

  scenario 'preview page' do
    @my_page.current_state
  end

  scenario 'validate main HTML nodes presence' do
    @my_page.validate_main_nodes
  end
end
Now let's imagine that we have simple search form on our page that we need to submit. 
First - define new nodes and method to fill in the form.

## my_page.rb [ruby_on_rails]
require "basic_page" 

class MyPage < BasicPage

  URL = '/my/url'
  CSS_WRAPPER = '#my'
  #
  # Attributes ( elements )
  #
  
  has_node :my_button, '#button'
  has_node :my_image,  '//img[contains(@src,"edit.png")]', :xpath
  has_node :my_link,   { :text => 'Save' }, 'a.my_css'
  # simple form
  has_node :my_button, '#button'
  has_node :my_input,  '#input'
  
  #
  # Methods ( actions )
  #
  def submit_simple_form(value='test')
    my_input.set(value)
    my_input.click
  end

  #
  # Validators ( checks)
  #

  def validate_main_nodes
    my_button_present?
    my_image_present?
    my_link_present?
  end

  def validate_successful_form_submit
    self.session.should have_content('Form submitted!')
  end
end

 Actions in action

## my_page_test.rb
feature 'Visitor works with my page' do
  before do
    login
    @my_page = MyPage.new( Capybara.current_session )
    @my_page.visit
  end

  scenario 'preview page' do
    @my_page.current_state
  end

  scenario 'validate main HTML nodes presence' do
    @my_page.validate_main_nodes
  end

  scenario 'successfuly submit form' do
    @my_page.instance_evel do
      submit_simple_form
      validate_successful_form_submit
    end
  end
end

Optimization

A few ideas to think.
- modules: common parts of the page can become modules
  that you'll include into your pages
- custom visit: url's can be dynamic so you'll need to define
  your own BasicPage::visit methods
- instance_eval: id doesn't always work well because of change
  in visibility scope
- Other?






Also tagged with one or more of these keywords: testing, rspec, poltergeist, integration

0 user(s) are reading this topic

0 members, 0 guests, 0 anonymous users