JSONQuerying Your Rails Responses

I’m writing an application right now that is really JSON heavy. Some of the functional tests are cucumber and some of them are just rails functional tests using shoulda.

I hit a point today where I wanted to verify that the JSON getting output was generally what I want. I could have just JSON parsed the response body and compared that with what I was looking for, but a little part of me thought this might be a cool application of JSONQuery.

JSONQuery provides a comprehensive set of data querying tools including filtering, recursive search, sorting, mapping, range selection, and flexible expressions with wildcard string comparisons and various operators.

The quote above is fancy and can be boiled down to “a query language for JSON”. If you want to read more about JSONPath and JSONQuery here are some posts:

Finding a Ruby JSONQuery Implementation

I knew there was a JavaScript implementation of JSONQuery and that Jon Crosby has been doing some cool stuff with it in CloudKit, but I couldn’t find a Ruby implemenation that didn’t require johnson.

After some googling and Github searching, I came across Siren. Siren was pretty much what I wanted, so I started playing around with it. I forked it, gem’d it and wrapped it with some shoulda goodness.

What I ended up with was pretty specific to my needs at the moment, but I post it here in hopes that it sparks some ideas.

Bringing It All Together

First, I added the following to my environments/test.rb file.

config.gem 'jnunemaker-siren',
            :lib     => 'siren',
            :version => '0.1.1',
            :source  => 'http://gems.github.com'

Then I added the following in my test helper (actually put it in separate module and file and included it but I’m going for simplicity in this post).

class ActiveSupport::TestCase
  def self.should_query_json(expression, string_or_regex)
    should "have json response matching #{expression}" do
      assert_jsonquery expression, string_or_regex
    end
  end
  
  def assert_jsonquery(expression, string_or_regex)
    json = ActiveSupport::JSON.decode(@response.body)
    query = Siren.query(expression, json)

    if string_or_regex.kind_of?(Regexp)
      assert_match string_or_regex, query, "JSONQuery expression #{expression} value did not match regex"
    else
      assert_equal string_or_regex, query, "Expression #{expression} value #{query} did not equal #{string_or_regex}"
    end
  end
end

The code is quick and dirty. The first thing you’ll notice is that assert_jsonquery actually uses @response.body, which means it can only be used in a controller test. I could easily expand it, but, like I said above, I just got it working for what I needed right now. The cool part is that now in my functional tests, I can do stuff like this:

context "on POST to :create" do
  setup { post :create, :status => {'action' => 'In', 'body' => 'Working on PB' }
  
  # ... code removed for brevity ...
  should_query_json "$['replace']['#current_status']", /Working on PB/
end

That is a really basic query. Trust me you can do a heck of a lot more. Check out the Siren tests if you don’t believe me.

Conclusion

Overall, working with siren was a little rough because I wasn’t familiar with JSONQuery syntax. Also, siren tends to return nil instead of a more helpful error message about my expression compilation failing, but I’m kind of excited to see how this works out in the long run.

What are you doing to test JSON in your apps? Does something like this seem cool or overkill? Just kind of curious.

3 Comments

  1. Nice solution. Since you asked, I have a Cucumber step that allows me to validate my JSON using xpath expressions. It parses the JSON into a hash, turns that into XML, and then queries it with the given xpath expression. (Since my JSON is typically actually JSONP, the code below removes my callback from the response.body with gsub before handing it to the JSON parser, or it will fail to parse.)

    
    Then /^I should get valid JSON$/ do
      assert_nothing_raised do
        assert JSON.parse(@response.body.gsub(/^#{JSON_CALLBACK}\((.*)\)/, '\1'))
      end
    end
    
    Then /^I should find "([^\"]*)" in JSON$/ do |xpath|
      assert !json_path(xpath, @response).blank?
    end
    
    Then /^I should find "([^\"]*)" at "([^\"]*)" in JSON$/ do |text, xpath|
      assert_equal json_path(xpath, @response).text, text
    end
    
    def json_to_xml(response)
      json_hash = JSON.parse(response.body.gsub(/^#{JSON_CALLBACK}\((.*)\)/, '\1'))
      Nokogiri::XML(json_hash.to_xml)
    end
    
    def json_path(xpath, response)
      json_hash = JSON.parse(response.body.gsub(/^#{JSON_CALLBACK}\((.*)\)/, '\1'))
      xmldoc = Nokogiri::XML(json_hash.to_xml)
      xpath.gsub!('_', '-') # in XML, underscores will be dashes.
      xpath = "./hash" + xpath unless xpath =~ /^\/\//
      xmldoc.xpath(xpath)
    

    end

  2. Hey, nice to see someone found a use for Siren. I started a few months back then got sidetracked by other projects. Maybe I should finish off the work I was in the middle of and document it. So little time…

  3. @James – Ha! I know the feeling. It is definitely coming in handy thus far.

Sorry, comments are closed for this article to ease the burden of pruning spam.

About

Authored by John Nunemaker (Noo-neh-maker), a programmer who has fallen deeply in love with Ruby. Learn More.

Projects

Flipper
Release your software more often with fewer problems.
Flip your features.