Testing and Stubbing the Geocoder gem
The geocoder gem is very useful tool in getting location information about your users.
Here’s a bit about testing and stubbing HTTP requests, and some of the problems when
pulling IP addresses in test
and development
environments.
Here’s my test:
# spec/features/user_location_spec.rb
require 'spec_helper'
feature "Users location is added to profile" do
scenario "while they're filling out the profile page" do
user = FactoryGirl.create(:user)
visit root_path
sign_in_as user
expect(page).to have_text
"Location: Cambridge, Massachusetts"
end
end
I’m asserting that I’ll have Cambridge, Massachussetts
printed on the page.
There are 2 parts to this process: setting, and lookup.
Setting
I want to get this information from the IP address. I’m collecting that information everytime a user logs on, here:
# app/controllers/sessions_controller.rb
def create
user = authenticate_session(session_params)
if sign_in(user)
current_user.ip_address = request_location
current_user.save
redirect_to root_path
else
@user = User.new
render [:new, :user]
end
end
We want to avoid passing Geocode.search
(coming up) the IP address 127.0.0.1
(which is our IP in test
and development
)
because it won’t even make a HTTP request that we can stub, it will just return nil. So instead we
do some situation handling here with request_location
which I define in ApplicationController
:
# app/controllers/application_controller.rb
def request_location
if Rails.env.development? || Rails.env.test?
"76.24.18.47"
else
request.location.data["ip"]
end
end
Now that we’ve changed the IP address for development
and test
we’re ready to make our Geocode request.
# app/models/user.rb
geocoded_by :ip_address
after_validation :geocode
In app/controllers/sessions_controller.rb
when we call current_user.save
this invokes
the after_validation :geocode
which pulls the ip_address
column from the current user
and sets the latitude
and longitude
fields in the database. It does this by making
and external HTTP request that we need to sub (because we’ve turned on Webmock to disallow
HTTP requests). Here’s how we configured webmock:
# spec/spec_helper.rb
require 'webmock/rspec'
WebMock.disable_net_connect!(allow_localhost: true)
RSpec.configure do |config|
config.before(:each) do
stub_request(:get, "http://freegeoip.net/json/76.24.18.47").
with(:headers => {'Accept'=>'*/*',
'User-Agent'=>'Ruby'}).
to_return(:status => 200,
:body => '{
"ip":"76.24.18.47",
"country_code":"US",
"country_name":"United States",
"region_code":"MA",
"region_name":"Massachusetts",
"city":"Cambridge",
"zipcode":"02138",
"latitude":42.38,
"longitude":-71.1329,
"metro_code":"506",
"area_code":"617"}',
:headers => {})
end
end
You can see that we’re stubbing a get
request to "http://freegeoip.net/json/76.24.18.47"
,
which contains the IP address we fed it. The other thing you can see is the :body
key
is pointing to a value that is a json
response. Geocode gives is a cool command line
helper to get the response we need to stub. By typing geocode -j 'you-search-term'
you can get the json
hash your app would get, allowing you to succesfully stub it. We passed it
our IP address to make sure it will stub the information our test expects.
Now what if we want to look up a users location to show them?
Lookup
Now that I have the information in the database, I want to pull it and show it on a users dashboard. Here is how I’ll do that:
# app/views/dashboards/show.html.erb
Location: <%= @location %>
# app/controllers/dashboards_controller.rb
@location = current_user.location
VERY simple. Let’s see what location
looks like for current_user
.
def location
result = Geocoder.search(ip_address).first
result.city + ", " + result.state
end
Passing the Geocoder.search
function our IP address will give us a Geocoder::Result object. We know that object responds to .city
and .state
and add some formatting.
With this set up our tests run green and we’re good to go!