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

Bit Vector Preferences

Posted by Jim Morris Wed, 08 Aug 2007 06:53:09 GMT

In my latest web project I potentially have a lot of boolean preferences, which I use for enabling or disabling various email notifications to users.

Rather than having to add a migration everytime I want to add a new preference, I thought I would use the composed_of feature in my model and compose the boolean preferences from a bitvector. That way I can simply modify my model to add new preferences rather than add new columns to the database.

I also wanted this to be easy to add new boolean preferences, so I use some Macros (I guess you could also call it Meta-Programming) to do all the repetitive code.

The result is a little class in my Person model called Preferences, one integer field in my persons database called preferences, and a composed_of :preferences in the Person model, and of course the following class in the person.rb model.

class Person < ActiveRecord::Base

  #...

  # place to store bit vector preferences
  # to add a new preference:-
  #   add symbol of preference to @@bits with bit allocation
  #   update initialize defaults if initial default is true
  class Preferences
    @@bits= {
      :comment_notifications => 1,
      :friendship_notifications => 2,
      :event_notifications => 4,
      :misc_notifications => 8 }

    # create a reader for each preference
    @@bits.each_key do |a|
      attr_reader a
    end

    # Initialize from integer or Hash
    def initialize(prefs)
      if prefs.nil?
        # set the defaults to false if not been set before
        @@bits.each do |a, v|
          instance_variable_set("@#{a}", false)  
        end
        # override default here        
        @comment_notifications= true
        @event_notifications= true
      elsif prefs.is_a?(Hash)
        # initialize from parameter Hash, and default to false if absent from hash
        @@bits.each do |a, v|
          instance_variable_set("@#{a}", false)  
        end

        prefs.each do |k,v|
          raise(ArgumentError, "Unknown preference #{k}") unless @@bits.has_key?(k.to_sym)
          instance_variable_set("@#{k}", true) if v == '1'
        end
      else
        # create from integer bit vector
        @@bits.each do |a, v|
          instance_variable_set("@#{a}", (prefs & v) != 0 ? true : false)  
        end
      end
    end

    # returns bit vector of preferences
    def preferences
      bv= 0
      @@bits.each do |a, v|
        bv |= instance_variable_get("@#{a}") ? v : 0  
      end
      return bv
    end

    # create a predicate for each preference
    @@bits.each_key do |a|
      alias_method((a.to_s + '?').to_sym, a)
    end
  end

  # access preferences as bit vector
  composed_of :preferences

  #...

end

All I need to do to add new preferences is add it to the @@bits class variable, which is a Hash of the preference name as a symbol and the bit it sets in the integer (actually the value of the bit, bit0 is 1, bit1 is 2 etc). The rest of the code is derived from the class variable.

To make things easier I also add a predicate for each preference, so I can access @person.preferences.comment_notifications? to see if any comment notifications are required for instance.

One other thing I do in the initialize method is set up defaults for the preferences. This is only really needed if it is being added as an after thought, and the column preferences is NULL in the database.

Because composed_of classes are immutable you must always create a whole new one to update them, so I also allow initialize to be called with a Hash, which can come straight from the controller. The last case of initialize is being passed the integer from the database, expanding it into the various boolean instance variables. The preferences method does the reverse and converts the boolean instance variables into the bit vector. Calls to these are all taken care of by ActiveRecord.

An example of it being called from the controller is...

@person.preferences= Person::Preferences.new(params[:preferences])

presuming you have a bunch of check boxes in your view which are passed in as part of the preferences hash.

Because I added this later I had one migration to initially add the new column...

add_column :people, :preferences, :integer

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

Bit Vector Preferences

Posted by Jim Morris Wed, 08 Aug 2007 06:53:09 GMT

In my latest web project I potentially have a lot of boolean preferences, which I use for enabling or disabling various email notifications to users.

Rather than having to add a migration everytime I want to add a new preference, I thought I would use the composed_of feature in my model and compose the boolean preferences from a bitvector. That way I can simply modify my model to add new preferences rather than add new columns to the database.

I also wanted this to be easy to add new boolean preferences, so I use some Macros (I guess you could also call it Meta-Programming) to do all the repetitive code.

The result is a little class in my Person model called Preferences, one integer field in my persons database called preferences, and a composed_of :preferences in the Person model, and of course the following class in the person.rb model.

class Person < ActiveRecord::Base

  #...

  # place to store bit vector preferences
  # to add a new preference:-
  #   add symbol of preference to @@bits with bit allocation
  #   update initialize defaults if initial default is true
  class Preferences
    @@bits= {
      :comment_notifications => 1,
      :friendship_notifications => 2,
      :event_notifications => 4,
      :misc_notifications => 8 }

    # create a reader for each preference
    @@bits.each_key do |a|
      attr_reader a
    end

    # Initialize from integer or Hash
    def initialize(prefs)
      if prefs.nil?
        # set the defaults to false if not been set before
        @@bits.each do |a, v|
          instance_variable_set("@#{a}", false)  
        end
        # override default here        
        @comment_notifications= true
        @event_notifications= true
      elsif prefs.is_a?(Hash)
        # initialize from parameter Hash, and default to false if absent from hash
        @@bits.each do |a, v|
          instance_variable_set("@#{a}", false)  
        end

        prefs.each do |k,v|
          raise(ArgumentError, "Unknown preference #{k}") unless @@bits.has_key?(k.to_sym)
          instance_variable_set("@#{k}", true) if v == '1'
        end
      else
        # create from integer bit vector
        @@bits.each do |a, v|
          instance_variable_set("@#{a}", (prefs & v) != 0 ? true : false)  
        end
      end
    end

    # returns bit vector of preferences
    def preferences
      bv= 0
      @@bits.each do |a, v|
        bv |= instance_variable_get("@#{a}") ? v : 0  
      end
      return bv
    end

    # create a predicate for each preference
    @@bits.each_key do |a|
      alias_method((a.to_s + '?').to_sym, a)
    end
  end

  # access preferences as bit vector
  composed_of :preferences

  #...

end

All I need to do to add new preferences is add it to the @@bits class variable, which is a Hash of the preference name as a symbol and the bit it sets in the integer (actually the value of the bit, bit0 is 1, bit1 is 2 etc). The rest of the code is derived from the class variable.

To make things easier I also add a predicate for each preference, so I can access @person.preferences.comment_notifications? to see if any comment notifications are required for instance.

One other thing I do in the initialize method is set up defaults for the preferences. This is only really needed if it is being added as an after thought, and the column preferences is NULL in the database.

Because composed_of classes are immutable you must always create a whole new one to update them, so I also allow initialize to be called with a Hash, which can come straight from the controller. The last case of initialize is being passed the integer from the database, expanding it into the various boolean instance variables. The preferences method does the reverse and converts the boolean instance variables into the bit vector. Calls to these are all taken care of by ActiveRecord.

An example of it being called from the controller is...

@person.preferences= Person::Preferences.new(params[:preferences])

presuming you have a bunch of check boxes in your view which are passed in as part of the preferences hash.

Because I added this later I had one migration to initially add the new column...

add_column :people, :preferences, :integer

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

Older posts: 1 2 3 ... 11