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
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
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
class HaveNodes
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 RSpec, Rails | Tags matcher, rspec, xpath | 2 comments | no trackbacks
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
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
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
class HaveNodes
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 RSpec, Rails | Tags matcher, rspec, xpath | 2 comments | no trackbacks
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
class Preferences
@@bits= {
:comment_notifications => 1,
:friendship_notifications => 2,
:event_notifications => 4,
:misc_notifications => 8 }
@@bits.each_key do |a|
attr_reader a
end
def initialize(prefs)
if prefs.nil?
@@bits.each do |a, v|
instance_variable_set("@#{a}", false)
end
@comment_notifications= true
@event_notifications= true
elsif prefs.is_a?(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
@@bits.each do |a, v|
instance_variable_set("@#{a}", (prefs & v) != 0 ? true : false)
end
end
end
def preferences
bv= 0
@@bits.each do |a, v|
bv |= instance_variable_get("@#{a}") ? v : 0
end
return bv
end
@@bits.each_key do |a|
alias_method((a.to_s + '?').to_sym, a)
end
end
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 Rails | Tags bitvector, composed_of, preferences, rails | 2 comments | no trackbacks
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
class Preferences
@@bits= {
:comment_notifications => 1,
:friendship_notifications => 2,
:event_notifications => 4,
:misc_notifications => 8 }
@@bits.each_key do |a|
attr_reader a
end
def initialize(prefs)
if prefs.nil?
@@bits.each do |a, v|
instance_variable_set("@#{a}", false)
end
@comment_notifications= true
@event_notifications= true
elsif prefs.is_a?(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
@@bits.each do |a, v|
instance_variable_set("@#{a}", (prefs & v) != 0 ? true : false)
end
end
end
def preferences
bv= 0
@@bits.each do |a, v|
bv |= instance_variable_get("@#{a}") ? v : 0
end
return bv
end
@@bits.each_key do |a|
alias_method((a.to_s + '?').to_sym, a)
end
end
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 Rails | Tags bitvector, composed_of, preferences, rails | 2 comments | no trackbacks