Swine Flu and the Twitter Gem

I had some extra time today and I’ve been spotty on open source work over the past few weeks, so I decided to add support for the Twitter trends API to my Twitter gem.

Using HTTParty, the code for this turned out to be insanely simple, so short, in fact, that I’ll just put it inline here so you don’t even have to go over to Github. Aww, I’m so nice.

module Twitter
  class Trends
    include HTTParty
    base_uri 'search.twitter.com/trends'
    format :json
    
    # :exclude => 'hashtags' to exclude hashtags
    def self.current(options={})
      mashup(get('/current.json', :query => options))
    end
    
    # :exclude => 'hashtags' to exclude hashtags
    # :date => yyyy-mm-dd for specific date
    def self.daily(options={})
      mashup(get('/daily.json', :query => options))
    end
    
    # :exclude => 'hashtags' to exclude hashtags
    # :date => yyyy-mm-dd for specific date
    def self.weekly(options={})
      mashup(get('/weekly.json', :query => options))
    end
    
    private
      def self.mashup(response)
        response['trends'].values.flatten.map { |t| Mash.new(t) }
      end
  end
end

Pure TDD

I am most definitely a tester, but I’ll admit I usually write code and then write the test. Of late, I’ve been reversing this trend and actually practicing TDD in full force by writing a small test, then only enough code to make it pass, followed by another test or more code for the existing test, finished with just enough code to make the new addition pass.

It is a different mindset to code in this way, compared to my code first and then make sure my butt is covered method and I’ve loving it. I thought I would find pure TDD tedious, but on the contrary, I think I’m coding faster and cleaner.

The Tests

So how did I test the code above? Again, inline for your viewing pleasure, are the tests I added to make sure I don’t break something in the future and get yelled at. Feel free to take a gander and I’ll meet back up with you at the bottom of it.

require File.dirname(__FILE__) + '/../test_helper'

class TrendsTest < Test::Unit::TestCase
  include Twitter
  
  context "Getting current trends" do
    should "work" do
      stub_get('http://search.twitter.com:80/trends/current.json', 'trends_current.json')
      trends = Trends.current
      trends.size.should == 10
      trends[0].name.should == '#musicmonday'
      trends[0].query.should == '#musicmonday'
      trends[1].name.should == '#newdivide'
      trends[1].query.should == '#newdivide'
    end
    
    should "be able to exclude hashtags" do
      stub_get('http://search.twitter.com:80/trends/current.json?exclude=hashtags', 'trends_current_exclude.json')
      trends = Trends.current(:exclude => 'hashtags')
      trends.size.should == 10
      trends[0].name.should == 'New Divide'
      trends[0].query.should == %Q(\"New Divide\")
      trends[1].name.should == 'Star Trek'
      trends[1].query.should == %Q(\"Star Trek\")
    end
  end
  
  context "Getting daily trends" do
    should "work" do
      stub_get('http://search.twitter.com:80/trends/daily.json?', 'trends_daily.json')
      trends = Trends.daily
      trends.size.should == 480
      trends[0].name.should == '#3turnoffwords'
      trends[0].query.should == '#3turnoffwords'
    end
    
    should "be able to exclude hastags" do
      stub_get('http://search.twitter.com:80/trends/daily.json?exclude=hashtags', 'trends_daily_exclude.json')
      trends = Trends.daily(:exclude => 'hashtags')
      trends.size.should == 480
      trends[0].name.should == 'Star Trek'
      trends[0].query.should == %Q(\"Star Trek\")
    end
    
    should "be able to get for specific date (with date string)" do
      stub_get 'http://search.twitter.com:80/trends/daily.json?date=2009-05-01', 'trends_daily_date.json'
      trends = Trends.daily(:date => '2009-05-01')
      trends.size.should == 440
      trends[0].name.should == 'Swine Flu'
      trends[0].query.should == %Q(\"Swine Flu\")
    end
    
    should "be able to get for specific date (with date object)" do
      stub_get 'http://search.twitter.com:80/trends/daily.json?date=2009-05-01', 'trends_daily_date.json'
      trends = Trends.daily(:date => Date.new(2009, 5, 1))
      trends.size.should == 440
      trends[0].name.should == 'Swine Flu'
      trends[0].query.should == %Q(\"Swine Flu\")
    end
  end
  
  context "Getting weekly trends" do
    should "work" do
      stub_get('http://search.twitter.com:80/trends/weekly.json?', 'trends_weekly.json')
      trends = Trends.weekly
      trends.size.should == 210
      trends[0].name.should == 'Happy Mothers Day'
      trends[0].query.should == %Q(\"Happy Mothers Day\" OR \"Mothers Day\")
    end
    
    should "be able to exclude hastags" do
      stub_get('http://search.twitter.com:80/trends/weekly.json?exclude=hashtags', 'trends_weekly_exclude.json')
      trends = Trends.weekly(:exclude => 'hashtags')
      trends.size.should == 210
      trends[0].name.should == 'Happy Mothers Day'
      trends[0].query.should == %Q(\"Happy Mothers Day\" OR \"Mothers Day\")
    end
    
    should "be able to get for specific date (with date string)" do
      stub_get 'http://search.twitter.com:80/trends/weekly.json?date=2009-05-01', 'trends_weekly_date.json'
      trends = Trends.weekly(:date => '2009-05-01')
      trends.size.should == 210
      trends[0].name.should == 'TGIF'
      trends[0].query.should == 'TGIF'
    end
    
    should "be able to get for specific date (with date object)" do
      stub_get 'http://search.twitter.com:80/trends/weekly.json?date=2009-05-01', 'trends_weekly_date.json'
      trends = Trends.weekly(:date => Date.new(2009, 5, 1))
      trends.size.should == 210
      trends[0].name.should == 'TGIF'
      trends[0].query.should == 'TGIF'
    end
  end
end

So, yeah, nothing earth shattering. It feels a bit repetitive, but I don’t mind some amount of repetition in my tests. The fixture files were created quite simply using curl.

cd test/fixtures
curl http://search.twitter.com:80/trends/weekly.json?date=2009-05-01 > trends_weekly_date.json
# rinse and repeat for each file

The stub_get method is a simple wrapper around FakeWeb and looks something like this:

def stub_get(url, filename, status=nil)
  options = {:string => fixture_file(filename)}
  options.merge!({:status => status}) unless status.nil?
  FakeWeb.register_uri(:get, url, options)
end

def fixture_file(filename)
  file_path = File.expand_path(File.dirname(__FILE__) + '/fixtures/' + filename)
  File.read(file_path)
end

I’m lazy and find that stub_get is much shorter than FakeWeb.register_uri blah, blah, blah. The tests use FakeWeb, shoulda and my fork of matchy, in case you are curious.

Example Uses

So what can you do with the new trends addition? Below are some examples of how you can obtain trend information.

Twitter::Trends.current
Twitter::Trends.current(:exclude => 'hashtags')

Twitter::Trends.daily # current day
Twitter::Trends.daily(:exclude => 'hashtags')
Twitter::Trends.daily(:date => Date.new(2009, 5, 1))

Twitter::Trends.weekly # current day
Twitter::Trends.weekly(:exclude => 'hashtags')
Twitter::Trends.weekly(:date => Date.new(2009, 5, 1))

That’s all for now. Enjoy the new trends and build something cool. Oh, and if you want to play with trends, but don’t have an idea, I have one and most likely won’t have time to build it. I’d be happy to collaborate.

2 Comments

  1. Sorry for this possibly stupid question, but I don’t understand how the get_stub is used with the matchers.

    How are the results of the stubs are passed?

  2. @Fadhli – The stub_get method makes it so that Twitter::Trends.current and such don’t actually hit the internet, but instead just return a fixture file locally so that the tests are predictable and run faster. FakeWeb does all the work behind the scenes. Check it out for more information.

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.