Wolfmans Howlings

A programmers Blog about Ruby, Rails and a few other issues

XPath matchers for rspec

Posted by Jim Morris Wed, 02 Jan 2008 21:32:50 GMT

UPDATE I found this which is a nicer way of doing it using hpricot, which is faster. I have created a slightly different version that does have_xpath, for XML.

I've been working on a project that is mostly Java for the last many months, so haven't had much Ruby or Rails stuff to share.

However one thing I found when working on my tests in Java was an xpath matcher for JUnit 4.0 using the Hamcrest libraries.

When I dropped back into Ruby to write some cron scripts that process information from the database and generate xml files I wanted to check the script with an rspec, and check the xml files it was generating.

To do this I wanted to use a similar matcher to the Hamcrest ones, but use it in RSpec.

I Googled around and found a simple example, but it wasn't very sophisticated and didn't check what I needed so I upgraded it to do the kind of matches I needed, the results are here.


require 'rexml/document'
require 'rexml/element'

module Spec
  module Matchers

    # check if the xpath exists one or more times
    class HaveXpath
      def initialize(xpath)
        @xpath = xpath
      end

      def matches?(response)
        @response = response
        doc = response.is_a?(REXML::Document) ? response : REXML::Document.new(@response)
        match = REXML::XPath.match(doc, @xpath)
        not match.empty?
      end

      def failure_message
        "Did not find expected xpath #{@xpath}"
      end

      def negative_failure_message
        "Did find unexpected xpath #{@xpath}"
      end

      def description
        "match the xpath expression #{@xpath}"
      end
    end

    def have_xpath(xpath)
      HaveXpath.new(xpath)
    end

    # check if the xpath has the specified value
    # value is a string and there must be a single result to match its
    # equality against
    class MatchXpath
      def initialize(xpath, val)
        @xpath = xpath
        @val= val
      end

      def matches?(response)
        @response = response
        doc = response.is_a?(REXML::Document) ? response : REXML::Document.new(@response)
        ok= true
        REXML::XPath.each(doc, @xpath) do |e|
          @actual_val= case e
          when REXML::Attribute
            e.to_s
          when REXML::Element
            e.text
          else
            e.to_s
          end
          return false unless @val == @actual_val
        end
        return ok
      end

      def failure_message
        "The xpath #{@xpath} did not have the value '#{@val}'\nIt was '#{@actual_val}'"
      end

      def description
        "match the xpath expression #{@xpath} with #{@val}"
      end
    end

    def match_xpath(xpath, val)
      MatchXpath.new(xpath, val)
    end

    # checks if the given xpath occurs num times
    class HaveNodes  #:nodoc:
      def initialize(xpath, num)
        @xpath= xpath
        @num = num
      end

      def matches?(response)
        @response = response
        doc = response.is_a?(REXML::Document) ? response : REXML::Document.new(@response)
        match = REXML::XPath.match(doc, @xpath)
        @num_found= match.size
        @num_found == @num
      end

      def failure_message
        "Did not find expected number of nodes #{@num} in xpath #{@xpath}\nFound #{@num_found}"
      end

      def description
        "match the number of nodes #{@num}"
      end
    end

    def have_nodes(xpath, num)
      HaveNodes.new(xpath, num)
    end

  end
end

The first matcher HaveXPath was pretty much the original I found on the net, it simply checks that an XPath exists, I don't use this one.

The next matcher MatchXPath is more like what I was using in Java, it gets an element from the xpath and checks the value is equal to the one expected string. I will eventually add regex matching and arrays of strings or regexs to check against.

The last one HaveNodes I find handy to make sure a given xpath matches the expected number of nodes.

I even wrote an rspec to check the matchers, and this also is handy to show the way to use them.

Note that you can pass the matchers a String containing the XML or (much faster) a REXML::Document.

require 'matchers'

describe "test matchers" do
  before(:each) do
    @xml= <<-EOFXML
    <?xml version='1.0'?>
      <claims>
        <testnode1>
          <day>
            <rank order='1' value='0' userid='26' alias='user25'/>
            <rank order='2' value='0' userid='93' alias='user92'/>
            <rank order='3' value='0' userid='55' alias='user54'/>
            <sometext>this is text</sometext>
          </day>
        </testnode1>
      </claims>
    EOFXML

    @doc= REXML::Document.new(@xml)

  end

  it "should test xpath" do
    @xml.should have_nodes("/claims/*", 1)
    @doc.should have_nodes("/claims/*", 1)
    @doc.should have_nodes("/claims/testnode1/day/rank", 3)
    @doc.should have_xpath("/claims/testnode1/day/rank[@order='1']")
    @doc.should_not have_xpath("/claims/testnode1/day/rank[@order='10']")
    @doc.should match_xpath("/claims/testnode1/day/rank[1]/@order", "1")
    @doc.should match_xpath("/claims/testnode1/day/rank[2]/@value", "0")
    @doc.should match_xpath("/claims/testnode1/day/rank[3]/@alias", "user54")
    @doc.should match_xpath("/claims/testnode1/day/sometext", "this is text")
  end

end

Posted in ,  | Tags , ,  | 2 comments | no trackbacks

XPath matchers for rspec

Posted by Jim Morris Wed, 02 Jan 2008 21:32:50 GMT

UPDATE I found this which is a nicer way of doing it using hpricot, which is faster. I have created a slightly different version that does have_xpath, for XML.

I've been working on a project that is mostly Java for the last many months, so haven't had much Ruby or Rails stuff to share.

However one thing I found when working on my tests in Java was an xpath matcher for JUnit 4.0 using the Hamcrest libraries.

When I dropped back into Ruby to write some cron scripts that process information from the database and generate xml files I wanted to check the script with an rspec, and check the xml files it was generating.

To do this I wanted to use a similar matcher to the Hamcrest ones, but use it in RSpec.

I Googled around and found a simple example, but it wasn't very sophisticated and didn't check what I needed so I upgraded it to do the kind of matches I needed, the results are here.


require 'rexml/document'
require 'rexml/element'

module Spec
  module Matchers

    # check if the xpath exists one or more times
    class HaveXpath
      def initialize(xpath)
        @xpath = xpath
      end

      def matches?(response)
        @response = response
        doc = response.is_a?(REXML::Document) ? response : REXML::Document.new(@response)
        match = REXML::XPath.match(doc, @xpath)
        not match.empty?
      end

      def failure_message
        "Did not find expected xpath #{@xpath}"
      end

      def negative_failure_message
        "Did find unexpected xpath #{@xpath}"
      end

      def description
        "match the xpath expression #{@xpath}"
      end
    end

    def have_xpath(xpath)
      HaveXpath.new(xpath)
    end

    # check if the xpath has the specified value
    # value is a string and there must be a single result to match its
    # equality against
    class MatchXpath
      def initialize(xpath, val)
        @xpath = xpath
        @val= val
      end

      def matches?(response)
        @response = response
        doc = response.is_a?(REXML::Document) ? response : REXML::Document.new(@response)
        ok= true
        REXML::XPath.each(doc, @xpath) do |e|
          @actual_val= case e
          when REXML::Attribute
            e.to_s
          when REXML::Element
            e.text
          else
            e.to_s
          end
          return false unless @val == @actual_val
        end
        return ok
      end

      def failure_message
        "The xpath #{@xpath} did not have the value '#{@val}'\nIt was '#{@actual_val}'"
      end

      def description
        "match the xpath expression #{@xpath} with #{@val}"
      end
    end

    def match_xpath(xpath, val)
      MatchXpath.new(xpath, val)
    end

    # checks if the given xpath occurs num times
    class HaveNodes  #:nodoc:
      def initialize(xpath, num)
        @xpath= xpath
        @num = num
      end

      def matches?(response)
        @response = response
        doc = response.is_a?(REXML::Document) ? response : REXML::Document.new(@response)
        match = REXML::XPath.match(doc, @xpath)
        @num_found= match.size
        @num_found == @num
      end

      def failure_message
        "Did not find expected number of nodes #{@num} in xpath #{@xpath}\nFound #{@num_found}"
      end

      def description
        "match the number of nodes #{@num}"
      end
    end

    def have_nodes(xpath, num)
      HaveNodes.new(xpath, num)
    end

  end
end

The first matcher HaveXPath was pretty much the original I found on the net, it simply checks that an XPath exists, I don't use this one.

The next matcher MatchXPath is more like what I was using in Java, it gets an element from the xpath and checks the value is equal to the one expected string. I will eventually add regex matching and arrays of strings or regexs to check against.

The last one HaveNodes I find handy to make sure a given xpath matches the expected number of nodes.

I even wrote an rspec to check the matchers, and this also is handy to show the way to use them.

Note that you can pass the matchers a String containing the XML or (much faster) a REXML::Document.

require 'matchers'

describe "test matchers" do
  before(:each) do
    @xml= <<-EOFXML
    <?xml version='1.0'?>
      <claims>
        <testnode1>
          <day>
            <rank order='1' value='0' userid='26' alias='user25'/>
            <rank order='2' value='0' userid='93' alias='user92'/>
            <rank order='3' value='0' userid='55' alias='user54'/>
            <sometext>this is text</sometext>
          </day>
        </testnode1>
      </claims>
    EOFXML

    @doc= REXML::Document.new(@xml)

  end

  it "should test xpath" do
    @xml.should have_nodes("/claims/*", 1)
    @doc.should have_nodes("/claims/*", 1)
    @doc.should have_nodes("/claims/testnode1/day/rank", 3)
    @doc.should have_xpath("/claims/testnode1/day/rank[@order='1']")
    @doc.should_not have_xpath("/claims/testnode1/day/rank[@order='10']")
    @doc.should match_xpath("/claims/testnode1/day/rank[1]/@order", "1")
    @doc.should match_xpath("/claims/testnode1/day/rank[2]/@value", "0")
    @doc.should match_xpath("/claims/testnode1/day/rank[3]/@alias", "user54")
    @doc.should match_xpath("/claims/testnode1/day/sometext", "this is text")
  end

end

Posted in ,  | Tags , ,  | 2 comments | no trackbacks

RSpec testing all actions of a controller

Posted by Jim Morris Sat, 28 Jul 2007 21:23:07 GMT

A pattern I find very helpful is to find all the actions in a controller and apply a test to all those actions.

For instance this is useful for automatically testing all actions are protected from unauthorized access when using a login system.

One nice feature of this pattern is that if you add an action to a controller it will automatically be tested. This is less helpful if you use

before_filter :login_required, :except => {...}

as it will automatically be protected, but there are other use cases where this is not the situation. Just as in the except clause above you need to explicitly add any action that does not need to be tested to an exception list, which is supported by this pattern.

Here are the methods I use to test for login accessibility.

module MySpecHelper

  # get all actions for specified controller
  def get_all_actions(cont)
    c= Module.const_get(cont.to_s.pluralize.capitalize + "Controller")
    c.public_instance_methods(false).reject{ |action| ['rescue_action'].include?(action) }
  end

  # test actions fail if not logged in
  # opts[:exclude] contains an array of actions to skip
  # opts[:include] contains an array of actions to add to the test in addition
  # to any found by get_all_actions
  def controller_actions_should_fail_if_not_logged_in(cont, opts={})
    except= opts[:except] || []
    actions_to_test= get_all_actions(cont).reject{ |a| except.include?(a) }
    actions_to_test += opts[:include] if opts[:include]
    actions_to_test.each do |a|
      #puts "... #{a}"
      get a
      response.should_not be_success
      response.should redirect_to('http://test.host/login')
      flash[:warning].should == @login_warning
   end
 end
end

I put this in my spec_helper.rb and include it as shown here:

describe "When Logged out" do
  include MySpecHelper
  controller_name :events

  before(:each) do
    controller.stub!(:current_user).and_return(:false)
    @login_warning= "You need to be logged in to do that"
  end

  # test all actions require login except the ones specified
  # add new_comment as it is not seen by the automatic collector
  it "actions should fail" do
    controller_actions_should_fail_if_not_logged_in(:input, 
                              :except => ['index', 'show', 'tagged'], 
                              :include => ['new_comment'])
  end
end

The get_all_actions method collects all the public un-inherited methods in the given controller, these will consist of all the accessible actions in that controller. I explicitly exclude rescue_action as it is created by RSpec itself and should not be tested. Note it will not see any actions that are in application.rb so you need to add those to the list manually of you want them tested. (See the :include option in the example).

The controller_actions_should_fail_if_not_logged_in could be put in the spec itself rather than the spec_helper, but as I call this from all my controller specs it is more DRY to put it here. This method takes the controller name and an option array of actions names to ignore. This method tests all the actions and makes sure I get the expected result of the filter failing due to not being logged in.

I show an example spec that uses this to test my events controller, it mocks the login calls to say I am not logged in, and then tests them with the exceptions of the actions in this controller that do not require one to be logged in.

This pattern can be extended to test all sorts of things, and is especially useful for testing things where you can add an action and forget to do something in a filter to protect it. Make sure the default is on the side of caution though. IE you need to explicitly except actions rather than include actions.

Another example is something I recently stumbled upon in my RESTful controllers. In many cases it is good to use a verify statement to make sure that the RESTful actions actually can only be called with PUT, POST or DELETE and fail if called with GET. I use this statement in my controllers to enforce this...

# GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
verify :method => :put, :only => [ :update ], :add_flash => { :error => "Operation Failed" }, :redirect_to => { :action => :index }
verify :method => :post, :only => [ :create, :new_comment ], :add_flash => { :error => "Operation Failed" }, :redirect_to => { :action => :index }
verify :method => :delete, :only => [ :destroy ], :add_flash => { :error => "Operation Failed" }, :redirect_to => { :action => :index }

I test this in my specs using this in the MySpecHelper Module

  def controller_actions_should_fail_with_get(cont, except=[])
    actions_to_test= get_all_actions(cont).reject{ |a| except.include?(a) }
    actions_to_test.each do |a|
      #puts "... #{a}"
      get a
      response.should redirect_to("http://test.host/#{cont.to_s.pluralize}")
      flash[:error].should == 'Operation Failed'
    end
  end

and an example of its use in a spec...

  it "actions should fail if not post or put" do
    controller_actions_should_fail_with_get(:event, ['index', 'show', 'edit', 'new'])
  end                                                                                     

Now whenever I add an action, the default is that it will fail with a GET, unless I add it to the exclude list in the spec, this will remind me to check if the action required PUT, POST or DELETE instead and to add it to the verify if so or add it to the specs exclude list if not.

These automatic tests keep me honest, especially in the last case where you really don't want a GET to be able to delete something.

I hope this pattern is useful to you.

Posted in ,  | Tags , ,  | 12 comments | no trackbacks

RSpec testing all actions of a controller

Posted by Jim Morris Sat, 28 Jul 2007 21:23:07 GMT

A pattern I find very helpful is to find all the actions in a controller and apply a test to all those actions.

For instance this is useful for automatically testing all actions are protected from unauthorized access when using a login system.

One nice feature of this pattern is that if you add an action to a controller it will automatically be tested. This is less helpful if you use

before_filter :login_required, :except => {...}

as it will automatically be protected, but there are other use cases where this is not the situation. Just as in the except clause above you need to explicitly add any action that does not need to be tested to an exception list, which is supported by this pattern.

Here are the methods I use to test for login accessibility.

module MySpecHelper

  # get all actions for specified controller
  def get_all_actions(cont)
    c= Module.const_get(cont.to_s.pluralize.capitalize + "Controller")
    c.public_instance_methods(false).reject{ |action| ['rescue_action'].include?(action) }
  end

  # test actions fail if not logged in
  # opts[:exclude] contains an array of actions to skip
  # opts[:include] contains an array of actions to add to the test in addition
  # to any found by get_all_actions
  def controller_actions_should_fail_if_not_logged_in(cont, opts={})
    except= opts[:except] || []
    actions_to_test= get_all_actions(cont).reject{ |a| except.include?(a) }
    actions_to_test += opts[:include] if opts[:include]
    actions_to_test.each do |a|
      #puts "... #{a}"
      get a
      response.should_not be_success
      response.should redirect_to('http://test.host/login')
      flash[:warning].should == @login_warning
   end
 end
end

I put this in my spec_helper.rb and include it as shown here:

describe "When Logged out" do
  include MySpecHelper
  controller_name :events

  before(:each) do
    controller.stub!(:current_user).and_return(:false)
    @login_warning= "You need to be logged in to do that"
  end

  # test all actions require login except the ones specified
  # add new_comment as it is not seen by the automatic collector
  it "actions should fail" do
    controller_actions_should_fail_if_not_logged_in(:input, 
                              :except => ['index', 'show', 'tagged'], 
                              :include => ['new_comment'])
  end
end

The get_all_actions method collects all the public un-inherited methods in the given controller, these will consist of all the accessible actions in that controller. I explicitly exclude rescue_action as it is created by RSpec itself and should not be tested. Note it will not see any actions that are in application.rb so you need to add those to the list manually of you want them tested. (See the :include option in the example).

The controller_actions_should_fail_if_not_logged_in could be put in the spec itself rather than the spec_helper, but as I call this from all my controller specs it is more DRY to put it here. This method takes the controller name and an option array of actions names to ignore. This method tests all the actions and makes sure I get the expected result of the filter failing due to not being logged in.

I show an example spec that uses this to test my events controller, it mocks the login calls to say I am not logged in, and then tests them with the exceptions of the actions in this controller that do not require one to be logged in.

This pattern can be extended to test all sorts of things, and is especially useful for testing things where you can add an action and forget to do something in a filter to protect it. Make sure the default is on the side of caution though. IE you need to explicitly except actions rather than include actions.

Another example is something I recently stumbled upon in my RESTful controllers. In many cases it is good to use a verify statement to make sure that the RESTful actions actually can only be called with PUT, POST or DELETE and fail if called with GET. I use this statement in my controllers to enforce this...

# GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
verify :method => :put, :only => [ :update ], :add_flash => { :error => "Operation Failed" }, :redirect_to => { :action => :index }
verify :method => :post, :only => [ :create, :new_comment ], :add_flash => { :error => "Operation Failed" }, :redirect_to => { :action => :index }
verify :method => :delete, :only => [ :destroy ], :add_flash => { :error => "Operation Failed" }, :redirect_to => { :action => :index }

I test this in my specs using this in the MySpecHelper Module

  def controller_actions_should_fail_with_get(cont, except=[])
    actions_to_test= get_all_actions(cont).reject{ |a| except.include?(a) }
    actions_to_test.each do |a|
      #puts "... #{a}"
      get a
      response.should redirect_to("http://test.host/#{cont.to_s.pluralize}")
      flash[:error].should == 'Operation Failed'
    end
  end

and an example of its use in a spec...

  it "actions should fail if not post or put" do
    controller_actions_should_fail_with_get(:event, ['index', 'show', 'edit', 'new'])
  end                                                                                     

Now whenever I add an action, the default is that it will fail with a GET, unless I add it to the exclude list in the spec, this will remind me to check if the action required PUT, POST or DELETE instead and to add it to the verify if so or add it to the specs exclude list if not.

These automatic tests keep me honest, especially in the last case where you really don't want a GET to be able to delete something.

I hope this pattern is useful to you.

Posted in ,  | Tags , ,  | 12 comments | no trackbacks

Older posts: 1 2 3