homeASCIIcasts

276: Testing Time & Web Requests 

(view original Railscast)

Other translations: Es Fr Ja

In the previous episode we demonstrated a workflow for test driven development. For the most part the pattern we used works well, but there are times when our applications will have functionality that is difficult to test. We’ll cover two of those scenarios in this episode.

Testing The Current Time

Last time we created a number of specs to test a User model. We wrote these fairly quickly and so we’ll go back to one of them now and look at it more closely.

/spec/models/user.rb

it "saves the time the password reset was sent" do
  user.send_password_reset
  user.reload.password_reset_sent_at.should be_present
end

This spec checks that when a password reset is sent the time it was sent at is saved in the password_reset_sent_at field. This is tested by using RSpec’s be_present matcher. This matcher calls a method that Rails provides called present? which checks for the existence of an object.

This spec is incomplete: it checks that a password_reset_sent_at value exists but not that it is set to the current time. We do set password_reset_sent_at to Time.zone.now in the User model’s send_password_reset method but the spec would pass no matter what value was set here. Ideally we should test that the value is the current time by writing something like this:

/spec/models/user.rb

it "saves the time the password reset was sent" do
  user.send_password_reset
  user.reload.password_reset_sent_at.should eq(Time.zone.now)
end

Unfortunately this won’t work. The current time at the time the test runs will be slightly after the time at which the code it’s testing executes. When faced with a problem like this it’s worth asking whether it’s worth adding complexity to the test in order to test it fully. In many cases, such as this one, it’s enough to test that the timestamp value exists. There’s very little possibility that there’ll be a bug in the one line of code that sets the time here, but there are times that we do need to test the current time so let’s investigate how we could do that in the spec.

Guard is running and we can see that the spec is failing. Although the timestamps are the same to the nearest second, they aren’t identical this is enough to cause the spec to fail.

Failures:
  1) User#send_password_reset saves the time the password reset was sent
     Failure/Error: user.reload.password_reset_sent_at.should eq(Time.zone.now)
       expected Mon, 25 Jul 2011 20:34:46 UTC +00:00
            got Mon, 25 Jul 2011 20:34:46 UTC +00:00
       (compared using ==)
       Diff:
     # ./spec/models/user_spec.rb:16:in `block (3 levels) in <top (required)>'
Finished in 1.95 seconds
9 examples, 1 failure

We can deal with this kind of problem by using a gem called Timecop. We can use it to manipulate the current time in many different ways, including freezing it. This means that we can freeze the current time while the spec runs so that the time at which the timestamp is set will be the same as it is when the value is checked.

We can add Timecop to our application by adding it to the Gemfile and running bundle. As we only need it for our tests we’ll add it to the test group.

/Gemfile

source 'http://rubygems.org'
gem 'rails', '3.1.0.rc4'
gem 'sqlite3'
# Asset template engines
gem 'sass-rails', "~> 3.1.0.rc"
gem 'coffee-script'
gem 'uglifier'
gem 'jquery-rails'
gem "rspec-rails", :group => [:test, :development]
group :test do
  gem "factory_girl_rails"
  gem "capybara"
  gem "guard-rspec"
  gem "timecop"
end

It’s a good idea now to go into the spec_helper file and add a call to Timecop.return in the config.before(:each) block. This ensures that any changes we make with Timecop are undone before each spec is run.

We can now call Timecop.freeze in any of our specs and freeze the current time while the spec runs. This means that we can compare both timestamps.

/spec/models/user.rb

it "saves the time the password reset was sent" do
  Timecop.freeze
  user.send_password_reset
  user.reload.password_reset_sent_at.should eq(Time.zone.now)
end

When Guard runs the specs now they all pass.

There is another unexpected scenario that can cause tests to fail. If you visit a Ruby conference in a different time zone you can suddenly find some of your time-related tests failing. Instead of travelling halfway around the world it’s a lot cheaper to set the timezone in the specs. We do this by setting Time.zone:

Time.zone = "Paris"

To set the zone temporarily within a single spec we can call Time.use_zone.

/spec/model/user_spec.rb

it "saves the time the password reset was sent" do
  Timecop.freeze
  user.send_password_reset
  Time.use_zone("Paris") do
    user.reload.password_reset_sent_at.should eq(Time.zone.now)
  end
end

The code inside the block will be run as if we were in Paris. We can use this to test that specs still pass even when we’re in a different time zone.

Whenever we test against the current time we should always use something like Timecop so that the time can be handled consistently and won’t change when we change time zones or during daylight saving time. We should also use it to test our code in different time zones so that we can check that our application works worldwide.

Testing External Web Requests

We’ll take a look next at testing external web requests. This section is based on a problem that Ryan Bates encountered when rewriting the Railscasts website, specifically the feature that shows the file size for each video format when you hover over the download link.

The file size is shown for each video format.

The file size is fetched from an external web server as the media files are hosted on a separate server. This means that an external web request is made and we’ll show now how this was tested. We’ll work on this through a small example application. This has a WebRequest resource and a form containing a text field that takes a URL.

Our web request application.

When we enter the URL of a video in the textbox and submit the form the URL is shown, along with the file’s size which is shown as zero bytes.

The file size is always shown as 0 bytes.

The file size is zero as we haven’t implemented this feature yet. We have a content_length method in our WebRequest model but this is hard-coded to return 0. We’ll implement this method now using TDD.

/app/models/web_request.rb

class WebRequest < ActiveRecord::Base
  def content_length
    0
  end
end

There are several gems that can help with testing external web requests, but we’re going to use Fakeweb. This gem can be used to register a URI and define what its response should be. When we use Net::HTTP to fetch that URI it will instead return the response that we defined instead of making an external request.

Fakeweb is installed in the usual way, by adding it to the Gemfile and running bundle. Next we’ll add some Fakeweb configuration by making two changes to the spec_helper file.

/spec/spec_helper.rb

ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'capybara/rspec'
Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
FakeWeb.allow_net_connect = false
RSpec.configure do |config|
  config.mock_with :rspec
  config.use_transactional_fixtures = true
  config.include(MailerMacros)
  config.before(:each) do
    Timecop.return
    reset_email 
    FakeWeb.clean_registry
  end
end

Near the top of the file we set Fakeweb.allow_net_connect to false which stops the specs from making any external HTTP connections. This is useful because it means that if we’ve left any external requests in the specs they won’t slow down the whole test suite and Fakeweb will let us know that the spec is trying to connect to the web. Inside the before(:each) we call Fakeweb.clean_registry so that each spec starts in the same state.

In the spec for WebRequest we’ll write a spec that tests that the content length is fetched.

/spec/models/web_request_spec.rb

require 'spec_helper'
describe WebRequest do
  it "fetches the content length" do
    FakeWeb.register_uri(:head, "http://example.com", :content_length => 123)
    WebRequest.new(:url => "http://example.com").content_length.should eq(123)
  end
end

We call FakeWeb.register_uri to register a fake URL here. The first argument this method takes is the type of request we want to make. We can get the file’s size from the header information and so we use :head here. The other arguments are the URL and any headers we want, in this case just the Content Length. We then create a new WebRequest object that calls this URL and check that the value returned by the content_length method is equal to the value set in the header.

Of course when we run this spec it fails as our content_length method always returns 0. To get the spec to pass we need to alter this method so that it returns the actual content length value for the file we’re requesting.

/app/models/web_request.rb

class WebRequest < ActiveRecord::Base
  def content_length
    uri = URI.parse(url)
    response = Net::HTTP.start(uri.host, uri.port) { |http| http.request_head(uri.path) }
    response["content-length"].to_i
  end
end

The method now calls Net::HTTP.start using the URL that was passed into the model. The block it takes calls request_head to get the response headers. Finally it returns the content-length header’s value.

Rails doesn’t include Net::HTTP by default and so we’ll need to require it in our application. We’ll do this in the application.rb file.

/config/auth.rb

require File.expand_path('../boot', __FILE__)
require 'net/http'
require 'rails/all'
# rest of file

Our specs all pass now and when we reload the page for our web request the correct file size is shown.

The correct file size is now shown.

If you ever need to handle external web requests in your tests, Fakeweb is a great solution.