Stubbing Twitter search in Rails 5

While working on a hobby project I ended up needing to stub out a search request to the Twitter API for testing purposes. I am using Rails 5, the twitter gem and Minitest.

There are some examples out there of mocking certain interactions of the Twitter client (this repo was very helpful), but I couldn’t find anything concerning search. I was trying to test a controller which only cared that it god a list of objects that responded to the text and uri methods, it didn’t know or care about the client or any interaction with Twitter.

Stubbing the Twitter client seemed like too much effort and not something that should belong in the controller test (this Reddit thread pointed me in the right direction). After some trial and error I settled on the following approach.

The controller’s only care is to serve some kind of array to the view:

	class TweetsController < ActionController::Base	
	  layout 'application'	
	
	  def search
	    @results = TwitterSearch.new.search(params[:q])	
	    render "tweets"
	  end	
	end

The Twitter search class deals with the actual communication with Twitter and handling of exceptions:

	class TwitterSearch
	  attr_reader :query

	  def client
	    @client ||= Twitter::REST::Client.new do |config|
	      config.consumer_key = ENV["CONSUMER_KEY"]
	      config.consumer_secret = ENV["CONSUMER_SECRET"]
	      config.access_token = ENV["ACCESS_TOKEN"]
	      config.access_token_secret = ENV["ACCESS_SECRET"]
	    end
	   end

	   def search(query)

	     begin
	       results = client.search("#{query}", result_type: "recent").take(3).collect
	     rescue
	       results = []
	     end
	   end
	end

For completeness, the view looks like this:

	h1 Tweets
	.container
	  = @results.each do |tweet|		
	    p
	      == tweet.text
	    a href=(tweet.uri)
	      == tweet.uri

Finally, this is the test that I ended up with:

	require 'test_helper'
	require 'minitest/mock'

	class TweetsControllerTest < ActionDispatch::IntegrationTest

	  test "should contain search parameter" do
	    Tweet = Struct.new(:text, :uri)
	    test_tweet = Tweet.new("This is a test tweet", "example.com")
	    twitter_search_stub = MiniTest::Mock.new
	    twitter_search_stub.expect :search, [test_tweet], ["testquery"]

	    TwitterSearch.stub :new, twitter_search_stub do
	      get "/tweets/search", params: {q: "testquery"}
	      assert_response :success
	      assert_select "p", "This is a test tweet"
              assert_select "a", "example.com"
	    end
	  end
	end

Let’s break that down a bit.

	Tweet = Struct.new(:text, :uri)
	test_tweet = Tweet.new("This is a test tweet", "example.com")

The view expects an object with the text and uri attributes. There is no Tweet class in the system, so for the purposes of the test we create one using Struct.

	twitter_search_stub = MiniTest::Mock.new
	twitter_search_stub.expect :search, [test_tweet], ["testquery"]

Create a mock that expects to respond to the search method, to return an array of tweets and to take an argument of “testquery”.

TwitterSearch.stub :new, twitter_search_stub do
       get "/tweets/search", params: {q: "testquery"}
       assert_response :success
       assert_select "p", "This is a test tweet"
       assert_select "a", "example.com"
     end

Use the mock stub out the TwitterSearch class for everything that happeds withing the block.

There we go, mocked out Twitter search in Minitest.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s