Skip to content

Cory Foy

Organizational agility through intersecting business and technology

Menu
  • FASTER Fridays
  • Mapping Mondays
  • Player Embed
  • Search Videos
  • User Dashboard
  • User Videos
  • Video Category
  • Video Form
  • Video Tag
Menu

Test-Driving Yahoo API Call with Ruby

Posted on June 10, 2006 by Cory Foy

I just started a new project for a web site (non-work related) I am very excited about. One of the things I have to do is make a call to the Yahoo Maps API to Geocode some information. I wanted to wrap the functionality into a class to make it easier to call from my app. I decided to spike a solution to see what it would take. So my first pass looked like:

require 'open-uri'
require 'rexml/document'
include REXML

class LatLngDecoder
  #...
  def retrieve
    open("http://yahooapi?city=#{@city}&state;=#{@state}") do |response|
      @status = response.status[STATUS_RESPONSE_CODE_POSITION]
      if @status == "200"
        xml = ''
        response.each_line do |line|
          xml += line
        end
        doc = Document.new(xmlString)
        @latitude = XPath.first(doc, "//ResultSet/Result/Latitude/text()")
        @longitude = XPath.first(doc, "//ResultSet/Result/Longitude/text()")
      end
    end
  end
end

Once I had it working, I tossed it out and decided to test drive it. The first piece I knew would be easy was the XPath expressions. I’m using the excellent REXML, and started by writing the following tests and making them pass:

require 'test/unit'
require 'LatLngDecoder'

class LatLngDecoderTest < Test::Unit::TestCase   def setup
    @latLng = LatLngDecoder.new("Test", "FL")
    @xmlString = "38.95-92.33"
  end

  def test_fills_lat
    @latLng.getLatLngFromXml(@xmlString)
    assert_equal(38.95, @latLng.latitude)
  end

  def test_fills_lng
    @latLng.getLatLngFromXml(@xmlString)
    assert_equal(-92.33, @latLng.longitude)
  end
end

To pass it, I just took the piece from the spike and put it in it’s own method. Unfortunately, that left me with the API call and processing the return element, which looked like:

open("http://yahooapi?city=#{@city}&state;=#{@state}") do |response|
  @status = response.status[STATUS_RESPONSE_CODE_POSITION]
  if @status == "200"
    xml = ''
    response.each_line do |line|
      xml += line
    end
  end
end

from the spike. I knew I needed a way to test that I was processing the response back from the api call correctly, without having to actually call it. Especially because I knew I would be writing tests for the status.

So I broke open my Programming Ruby book, but couldn’t find what type of object the open-uri’s open call passed to the block. Then I dug up the docs for open-uri, and found that:

OpenURI::OpenRead#open returns an IO like object if block is not given. Otherwise it yields the IO object and return the value of the block. The IO object is extended with OpenURI::Meta.

Aha! So I pulled up irb, and found out that the open call returned a StringIO object. Further, from the docs, it looks like open-uri mixes in OpenURI::Meta. Knowing that, I was able to write my next test:

def test_get_xml_from_io
  mockIO = MockStringIO.new(@xmlString)
  mockIO.status = ["200", "Success"]
  @latLng.process_response(mockIO)
  assert_equal("200", @latLng.status)
  assert_equal(38.95, @latLng.latitude)
  assert_equal(-92.33, @latLng.longitude)
end

So my test wanted a method which got passed an IO object it could then pull out the methods from. Creating the MockStringIO object was amazingly simple. How simple? Here is the entire class (don’t blink):

require 'open-uri'
class MockStringIO < StringIO
  include OpenURI::Meta
end

Yep! That’s it. With that, I was able to get the class down to:

def retrieve
  open("http://yahooapi?city=#{@city}&state;=#{@state}") do |response|
    process_response(response)
  end
end

def process_response(response)
  @status = response.status[STATUS_RESPONSE_CODE_POSITION]
  if @status == "200"
    xml = ''
    response.each_line do |line|
      xml += line
    end
    getLatLngFromXml(xml)
  end
end

def getLatLngFromXml(xmlString)
  doc = Document.new(xmlString)
  @latitude = XPath.first(doc, "//ResultSet/Result/Latitude/text()")
  @longitude = XPath.first(doc, "//ResultSet/Result/Longitude/text()")
end

So still not perfect (I couldn’t figure out how to test drive the actual call without either doing a form of D/I or writing an acceptance test), but I was pretty happy with the results. And being able to mock the IO object was just an absolute joy.

I’m looking forward to getting the new project online. It’s not anything big, but I think it will have some cool features that I am pumped about.


Follow-up: I found that I had a bug in the program anyway, and it was in the untested part of the code (imagine that!). The correct way to expand variables is “#{variable}” not “${variable}” which was just a typo on my part, but something I had no tests to catch.

I caught it by doing some manual testing, so I extracted out the creation of the URL into a method which I could then test that the variables were being created correctly. Another great example of where we shouldn’t write code without a failing test.

3 thoughts on “Test-Driving Yahoo API Call with Ruby”

  1. James Carr says:
    June 11, 2006 at 2:19 pm

    I think you just awoken my interest in ruby again … I had to do the same thing for a friend but using php and suffice to say I accomplished the same thing with 200% more bloat. ;)

  2. Cory Foy says:
    June 11, 2006 at 2:56 pm

    Thanks James! That’s one of the things I really love about Ruby – it stays quite out of your way and really lets you build stuff quickly.

    I put together a rough draft of some of the functionality of Friday, and was able to get it up and running within 2 hours. It’s a great language! ;)

  3. Anonymous says:
    July 27, 2006 at 11:05 pm

    One simple troubleshooting method I use is to call the “class” method on objects that appear to be returning a type I am not expecting. Just a simple “puts object.class” can save time going through docs!

Comments are closed.

© 2025 Cory Foy | Powered by Superbs Personal Blog theme