Mocking Twitter search in Rails
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, 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. 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.