views:

175

answers:

2

I have a shoulda macro/method that tests controller responses for XPath conditions like so:

def self.should_have_xpath(path, &block)
  should "have an element matching the path: #{path}" do
    xml = REXML::Document.new @response.body
    match = XPath.match(xml, path)
    if block_given?
      yield match
    else
      assert match.size != 0, "No nodes matching '#{path}'"
    end
  end
end

The XPath matching and built-in assertion works great. However, I have one testcase where I want exactly one match element to exist. This is the job of the optional block: expose the XPath match to the caller so that it can perform additional/context-specific assertions.

Unfortunately, when I actually pass a block:

should_have_xpath("//my/xpath/expression") { |match| assert_equal 1, match.size }

... I get this error:

NoMethodError: undefined method `assert_equal' for Users::SessionsControllerTest:Class

This is (as I understand it) because of the way Shoulda works: the parameters passed to the "should" call (including the block) are defined in the context of the test class and not the instance of the test class. Test::Unit::Assertions.assert* are module instance methods, so I can't access them conveniently.

So, my question is: Is there a convenient/idiomatic way to access the assert* methods from Test::Unit::Assertions without too much trouble? The solution has to work with Shoulda, though it doesn't necessarily need to be dependent on Shoulda; a straight Ruby way would be fine.

A: 

I've got a working solution:

def self.should_have_xpath(path, &block)
  should "have an element matching the path: #{path}" do
    # ...
    yield match, self
  end
end

should_have_xpath( "//my/xpath/expression" ) { |match, test | test.assert_equal 1, match.size }

I don't especially like having to (remember to) pass the test instance every time, which is why I'm hoping Stack Overflow will be able to come up with a better answer.

Craig Walker
+1  A: 

This should work as you want it to:

def self.should_have_xpath(path, &block)
  should "have an element matching the path: #{path}" do
    # ...
    block.bind(self).call(match)
  end
end
Paul McMahon
Perfect! Thanks a bundle.
Craig Walker