Arriving at a DSL for XPath Testing in Rails

February 17, 2008 | code, rails, ruby, testing, xml

Recently I was writing custom Xml exporting for some of our Rails models. I favor TDD over just shoot-from-the-hip development, so I had to write some tests. For testing the validity of the XML, I chose XPath for its simplicity.

As I added functionality (and thereby added tests) I noticed that my tests were becoming increasingly redundant, and that Test::Unit’s syntax was making my test files ugly and more importantly hard to read. As I started DRY-ing up the individual tests and trying to find better ways to share code and make the tests more succinct, I noticed that I was driving very close to creating my own mini-DSL for testing Xml with Xpath.

My Requirements for Xml Exporting

  1. A required element must always exist in the resulting Xml regardless of the model’s state
  2. An optional element:
    • Must exist when the model meets a certain state condition (i.e. the member is not null)
    • Must not exist when that condition is not met

A DSL That Meets My Testing Needs

As mentioned earlier, this DSL evolved from repeated refactoring my tests in order to make them more clear. Here is an example of an XPath-based Xml test:


xpath_tests_for @video_library do
  # ensure that the Xml always contains:
  #   <library><videos></videos</library>
  require '/library/videos'

  # ensure that when certain conditions are met, the Xml contains:
  #   <library><genres></genres></library>
  optional '/library/genres' do
    before :test_missing do |library|
      library.genres.clear
    end

    before :test_existing do |library|
      library.genres << Genre.new :name => "Comedy"
    end
  end
end

Here’s a run-down of the language elements in this mini-DSL:

xpath_tests_for @model
Describe tests for a given instance of a model

require <xpath>
Add a test requiring the xpath in the test-suite’s Xml blob

optional <xpath>
Add a test requiring the xpath in the Xml under certain conditions

before :test_missing
A block defining the conditions whereby the element must be missing in an optional test

before :test_existing
A block defining the conditions whereby the element must be existing in an optional test

How This DSL Emerged

As I mentioned at the start of this article, I didn’t approach this testing with a DSL in mind, the entire concept emerged from my testing. Here is an example of the emergent nature of this DSL. As seen in the earlier example, I can test all of my requirements, but here is a new requirement:

Since this is custom Xml generation for a specific target application, do not change the default behavior of ActiveRecord#to_xml

As you might have imagined the testing currently functions with the test runner calling ActiveRecord’s built-in to_xml method, and my Library model had code in it to generate the custom xml format.


# somewhere in the xpath test runner
xml_for_testing = @video_library.to_xml

# models/library.rb
def to_xml
  # code to render custom xml format
end

However, in order to meet the requirement, I had to change my model, moving my custom Xml generation from to_xml to some new method:


# models/library.rb
def generate_custom_xml
  # code to render custom xml format
end

Now I’m meeting the requirement, but the tests are broken because the test runner is now calling the wrong method for my tests

In order to accomodate this change, I updated my DSL to include a block for specifying how xml should be generated:


xpath_tests_for @video_library do
  generate_xml_as do |library|
    library.generate_custom_xml
  end

  require '/library/videos'
  ...
end

This way I my test runner can yield the data model to the generation block and the block can handle the specifics of using the model to generate xml.

Note: Because i’m doing “red/green” test-driven-development, I actually wrote the new test syntax first and the code later, but for clarity, wrote the article in the opposite order.

Final Notes

If I wasn’t practicing TDD, I probably would have never come up with a succinct mini-language for XPath-based testing. During the initial design of this feature, it would have seemed like an over-engineering. Who wants to put that much effort in testing anyway? However, since the code emerged as necessary from my test-driven-development, I was not only able to write some interesting testing code, but I also now have new plugin for performing XPath testing in other apps.

Not only is this a personal win for TDD in my book, but since this is my first DSL (thanks Ruby!) I learned a few things about development with a DSL. Most notably, since the DSL defines an ideal API, I rarely if ever changed the code in my tests. Most of the work I did to get this functional and optimized was in the test-runner and supporting classes. And all of that code was able to be written without changing my tests

Post a Comment