9780596521424
testing_id64728.html

Chapter 8. Testing

Testing is about asserting that certain expectations come true – that code works the way you expect it to when it runs.

Writing good tests means that you’ll find yourself catching more bugs, re-thinking your approach when you realize your code isn’t easily testable (a red flag that maintenance down the line is going to be rough), and having something to tell you that your application isn’t completely broken after you’ve changed or added a feature.

The typical workflow for testing looks like this:

  1. Write a test or refactor an existing one

  2. Run the test, watch it fail

  3. Make your code pass all tests

Polishing and refactoring your application becomes a matter of repeating these steps, with the added bonus of staying focused while you write code (all you have to do is pass the test) as well as keeping your code tested.

To aid you in reaching this lofty goal of testing-nirvana, Rails comes with a suite of tools to get you there.

Let’s take a look at an example model called Article:

class Article < ActiveRecord::Base
  belongs_to :author, :class_name => "User"
  
  named_scope :recent_articles, {:limit => 5, :order => 'created_at DESC'}
  
  def popular?
    times_read > 25
  end
end

Its matching test, ArticleTest (kept under test/unit/article_test.rb), looks like this:

# Bring in the extra helper code we created
# in test/test_helper.rb
require 'test_helper'

# The test class (here, ArticleTest) must be in a file called
# article_test.rb for the "rake test" tasks to run correctly
class ArticleTest < ActiveSupport::TestCase

  def setup
    # Code in here is executed before each test
    
    # Instance variables set here are available
    # in all tests
    @article = Article.new
  end

  test "#popular? returns true when times_read > 25" do
    
    # This test shows the general testing pattern:
    
    # 1) Set up a test situation
    @article.times_read = 26
    
    # 2) Assert that given the situation, some method
    # acts the way you expect
    assert @article.popular?
  end
  
  # Another test for an alternative situation
  test "#popular? false when times_read <= 25" do
    @article.times_read = 25
    assert !@article.popular?
  end
end

Each model has its behaviour tested by a corresponding test class. Each test class is composed of test methods, defined by using the test method with a descriptive string and a block of test code. Each test method contains assertions, which define conditions or expected results that must be met for the test to pass.

Note that all tests must have a class-name that ends with “Test”, as in SomethingTest or FooBarTest and that their containing filename must match it in snake_case format (e.g. ArticleTest goes in article_test.rb).

When running article_test.rb, each test method in each TestCase class is executed. When an assertion fails, the containing method stops executing and an error message is displayed. The rest of the test method won’t be executed, but the rest of the test methods in the class will.

Here’s an example of the tests in article_test.rb failing:

$ rake test:units
Started
EE
Finished in 0.013444 seconds.

  1) Error:
test_#popular?_false_when_times_read_<_25(ArticleTest):
NoMethodError: undefined method `times_read=' for #<Article id: nil, created_at: nil, updated_at: nil>
    test/unit/article_test.rb:31:in `test_#popular?_false_when_times_read_<_25'

  2) Error:
test_#popular?_true_when_times_read_>_25(ArticleTest):
NoMethodError: undefined method `times_read=' for #<Article id: nil, created_at: nil, updated_at: nil>
    test/unit/article_test.rb:22:in `test_#popular?_true_when_times_read_>_25'

2 tests, 0 assertions, 0 failures, 2 errors

Looks like we’ve forgotten to add a times_read field to the articles table. Oops. Our assertions didn't even have a chance to pass or fail, because the missing method threw an error before they could be evaluated.

Note

Earlier versions of Rails test methods looked slightly different, but did the same thing:

# Older style
def test_article_without_a_title_should_be_invalid
  article = Article.new
  assert article.title.blank?
  assert !article.valid?
end

The Basics

Typical Rails tests come in the following forms:

Unit (Model)

These test business logic in your model layer. They are called unit tests because each one should be testing the smallest units of behaviour in your application.

Functional (Controller)

These test your application's controller actions one at a time. They are called functional tests because they test the user-facing functionality of your application.

Integration (Controller to Controller)

These test a whole flow of user interaction from screen to screen. They are a chance to test your application as an integrated whole to make sure that the pieces fit together in the right way.

In other words, the basic relationships look like this:

Model Unit Test
Controller Functional Test
View (as part of a) Functional Test
Controller to Controller Integration Test

Note

Unit tests are also used for testing non-Model code (like classes you might put under /lib) that don’t fall under any of these Model/View/Controller categories.

If you use a Rails generator to create a model or controller, you’ll find that it also generates corresponding test files for you. Here's the resource generator creating a model-controller pair along with tests for both of them:

$ rails generate resource Article
      invoke  active_record
      create    db/migrate/20100712164342_create_articles.rb
      create    app/models/article.rb
      invoke    test_unit
      create      test/unit/article_test.rb
      create      test/fixtures/articles.yml
      invoke  controller
      create    app/controllers/articles_controller.rb
      invoke    erb
      create      app/views/articles
      invoke    test_unit
      create      test/functional/articles_controller_test.rb
      invoke    helper
      create      app/helpers/articles_helper.rb
      invoke      test_unit
      create        test/unit/helpers/articles_helper_test.rb
       route  resources :articles

The Article model is generated along with a file for its unit tests at test/unit/article_test.rb. A fixtures file is also generated for the model at test/fixtures/articles.yml. Fixtures are serialized record instances stored in YAML files which get loaded into the test database as part of the setup process before each test is run.

ArticlesController gets a place to put its functional tests at test/functional/articles_controller_test.rb, and its associated view helpers can be unit tested in test/unit/helpers/articles_helper_test.rb.

In all, we got four new files test directory from generating that resource:

  • test/unit/article_test.rb

  • test/fixtures/articles.yml

  • test/functional/articles_controller_test.rb

  • test/unit/helpers/articles_helper_test.rb

Missing here are integration tests, which are not explicitly tied to particular controllers or models and so are not generated along with them.

Unit Tests

Unit tests verify logic and behavior defined in your models by evaluating them in isolation, without the overhead of controllers, views, or sessions. These tests are the foundation of your test suite.

Assertions

These are the essential assertions you’ll be using in your test methods:

Common assertions

assert(boolean, message = "")

Passes when boolean is true. Displays message on failure or "boolean is not true."

assert @article.popular?
assert_nil(obj, message = "")

Passes when obj is nil.

assert_nil @article.editor, "no editor expected"
assert_equal(expected, actual, message = "")

Passes when expected == actual.

assert_equal expected_readers, @article.readers
assert_not_equal(expected, actual, message = "")

Passes when expected != actual.

assert_not_equal @different_article.readers, @article.readers
assert_match(pattern, string, message = "")

Passes when string matches the regular expression pattern.

assert_match(/edward/i, @article.author)
assert_no_match(pattern, string, message = "")

Passes when string does not match the regular expression pattern.

assert_no_match(/bill/, @article.author)
assert_in_delta(expected_float, actual_float, delta, message = "")

Passes when expected_float and actual_float are no further apart than delta.

assert_in_delta @comment.spamminess, (5.0/3.0), 0.2
assert_raise(expectation, ...) { block }

Passes when the block raises one of the supplied exceptions.

assert_raise TooAwesomeException do
  Article.create(:title => "Vampire Sharks With Bears for Arms")
end
assert_nothing_raised(expection, ...) { block }

Passes when the block does not raise one of the supplied exceptions.

assert_nothing_raised do
  @boring_story.destroy
end
assert_difference(expression, difference = 1, message = nil) { block }

Takes a Ruby expression as a String or an Array of expressions and passes when the value returned by the expression before and after the block changes by difference. Note that if you leave out the difference argument, that doesn't mean "assert any non-zero difference"—it will simply default to an expectation of 1, and will fail if the difference turns out to be any other value.

assert_difference 'Article.count' do
  post :create, :article => valid_params
end

assert_difference 'Article.count', -1 do
  post :delete, :id => @boring_article.id
end

assert_difference ['Article.count', 'AdSpace.count'], +2 do
  post :create, :article => valid_params
end

Look up the Test::Unit::Assertions module (included in Ruby’s standard library) for more assertions available for use in Unit Tests.

Test Helper Methods

Writing custom assertions and helper methods can keep your tests readable, small, and DRY. The place to keep those custom assertions and helper methods is in test/test_helper.rb, loaded before any test is run.

Here’s an example of adding a custom assertion for testing the size of collections. Add this to test/test_helper.rb:

class ActiveSupport::TestCase
  def assert_size(collection, size, message = nil)
    message ||= "expected #{collection.inspect} to have #{size} elements but instead had #{collection.size}"
    assert_block message do
      collection.size == size
    end
  end
end

With that code in place, the assert_size method is now available in all our unit, functional, and integration tests:

require 'test_helper'
class ArrayTest < ActiveSupport::TestCase
  test "array length" do
    assert_size [1,2,3], 3
  end
  
  test "Article.recent_articles should only return 5 records" do
    assert_size Article.recent_articles, 5
  end
end

Fixtures

Preparing and populating model data in each test is difficult to refactor, prone to mistakes, and overall, a giant pain. Rails’ built-in answer is the concept of fixtures – files that hold model data in a human-readable format called YAML. When these fixtures are used in a test, out come ActiveRecord objects from the test database, just like if we had generated the objects manually.

Before each test is run, the test database is emptied, then loaded with data defined in each model’s respective fixture.

Each record is defined using a sensible name, and the attributes you wish to load into the database.

An example User model fixture (found in test/fixtures/users.yml) where two users, admin and bob, are defined looks like this:

admin:
  login: admin
  password: secret
  adminstrator: true

bob:
  login: bobby
  password: password

Note that any attributes not provides in the fixture will use the defaults defined in the database schema.

Using fixtures in tests

A fixture record is referred to in a test by calling name_of_fixture(:name_of_fixture_record) – each of these methods return a corresponding ActiveRecord object loaded with the defined fixture data. (These methods named after the fixture files are dynamically generated by Rails at runtime.)

Here’s an example of using the User fixture in a test where we load admin to set the stage for one situation and bob for another:

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  
  test "adminstrators should be able to delete articles" do
    assert users(:admin).delete_article( articles(:ruby_wins) )
  end
  
  test "regular users should not be able to delete articles" do
    assert !users(:bob).delete_article( articles(:ruby_wins) )
  end
  
end

You could write a test that was functionally equivalent using normal ActiveRecord finders like User.find(1) and Article.find(3), the resulting code would be much less readable. Good tests read like a story, and naming fixtures appropriately is a big part of that. Avoid generic fixture names like :first or :one, and never use ActiveRecord finders with hard-coded IDs like Article.find(3) in your tests.

Warning

Fixtures are loaded directly into your test database, bypassing any validations you may have defined in your models, so it’s easy to accidentally populate your database with invalid model data.

It’s tempting to take advantage of this “feature” to define invalid fixtures for testing validations, but don’t do it. Because your fixtures are shared between your unit, functional, and integration tests, an invalid fixture will likely break something down the line. If you’re going to test validations, do something like this:

test "all users should have a login" do
  user = users(:bob)
  user.login = nil
  assert !user.valid?

  user.login = "validLoginName"
  assert user.valid?
end

Unit testing should reveal bugs in your code, not your test data. Unless you're designing an app that expects to find bad data in the database, just don't do it.

Defining relationships between fixtures

Giving your fixtures sensible names pays off again when you have define associations between your fixtures. You could define all the ids manually. This example sets up an Article authored by User bob by specifying what the ids should be in the database:

bob:
  id: 2
  login: bobby
  password: password
ruby_wins:
  id: 3
  author_id: 2
  title: World's only remaining Java programmer finally learns Ruby

Juggling all those identical looking ids gets confusing fast. Thankfully, you can omit those id attributes and just use the names of the associations and fixtures:

bob:
  login: bobby
  password: password
the_final_countdown:
  author: bob
  title: World's ultimate heroes all clash in the most epic battle ever.

Rails uses the names of the fixtures to generate the missing IDs. If you need to find out what the ID for a particular fixture name would be, you can use ActiveRecord::Fixtures.identify, which is made available when test_helper.rb is loaded:

ActiveRecord::Fixtures.identify("bob")                 #=> 902541635
ActiveRecord::Fixtures.identify("the_final_countdown") #=> 1020415498
ActiveRecord::Fixtures.identify("abcdefg")             #=> 824863398

Factories: a fixture alternative

While fixtures are great for managing small sets of sample test-data, they often grow unwieldy as a project scales in size. If I add Bob to the best-sellers group, will other tests start to break? What if I add another user, changing the total count? It’s hard to know exactly where and how a fixture is being used, so making changes can have vast unintented consequences. Things get even scarier when you remember that Rails will happily load invalid fixtures into your database. Something as simple as a new validation can break tons of tests in other parts of your app.

Model factories like Pat Nakajima’s Fixjour and Thoughtbot's Factory Girl alleviate issues you’ll run into with fixtures as your project grows. Their approaches typically involve giving you methods for each of your models to

  • Get a hash of valid attributes

  • Instantiate a new valid model object

  • Create a valid model and save it to the database

With these available, you’ll run into fewer problems due to your model objects being invalid without you knowing.

Another problem with named fixtures is that you end up being tempted to name them things like user_who_likes_donuts_but_not_cake, where not only is the fixture name really long in order to be descriptive, but the fixture doesn’t actually represent that situation, or does, but also has other special characteristics that have been added for a corner case. These disingenuous fixture names lead to all sorts of bad surprises that eat into your development time.

Factories avoid these situations by encouraging the test code to be explicit in defining the constructed model. While it involves more code up front (i.e. create_user(:admin => true) instead of admin_user), the intent is more evident, and could also suggest that we’re really just missing an Admin class that inherits from User.

More explicit details on how to set up and use these popular factory gems are available on the web.

Functional Tests

Functional tests assert expectations of controller actions and their responses, typically generated by their corresponding views. Here's an example functional test for ArticlesController:

require 'test_helper'

class ArticlesControllerTest < ActionController::TestCase
  
  setup do
    # Prepare an object/context for testing
    @article = Article.find(:first)
  end
  
  test "#show fetches the article with the given ID" do
    # Make a GET/PUT/POST/DELETE with that object
    # that corresponds to an action
    get 'show', :id => @article.id
    assert_response :ok
    
    # fetch the @article variable which was assigned
    # in the controller action
    assert_equal @article, assigns(:article)
  end
  
  test "#index fetches articles" do
    get 'index'
    assert_equal Article.all, assigns(:articles)
  end
end

Note that the controller being tested is inferred from the test class’ name: ArticlesControllerTest will implicitly test the ArticlesController class (i.e. get calls and the like will be aimed at it).

To set the controller specifically, use the tests statement in your test:

require 'test_helper'

class SomeOtherControllerTest < ActionController::TestCase
  tests ArticlesController
end

Special variables available in functional tests include:

@controller

The controller the test is about

@request

The request just made to the controller being tested (accessible only after a request has been issued though a get call or similar)

@response

The response from the controller. This huge object (take a look for yourself with a puts @response.inspect) has a reference to anything you ever wanted to know about the results of the request that just happened. Particularly useful bits include @response.body and @response.headers.

Testing internal controller state is generally done using the assertions mentioned previously in Unit Testing, and the following methods and special assertions:

assigns(key)

Used to access instance variables set in the tested controller action. (Note that it’s the only method of the bunch; the rest work like Hashes.)

# Expects @article.title to be set in the controller
# 
#   Note that assigns takes only Strings, unlike the others
#   which can use either symbols or Strings when naming 
#   the controller instance variable they return.

assert_equal expected_title, assigns('article').title

session

Used to access the session in the tested controller action.

# Asserts that there's a user id set in the session
assert session['user_id']
flash

Used to access the flash in the tested controller action.

# Assert that the flash contains meaningful feedback
assert_match /account updated/, flash['info']
cookies

Used to access the cookies in the tested controller action.

assert_equal expected_favourite_colour, cookies['favourite_colour']
request

Used to access the last request made in the tested controller action. (So be sure to have made a get or similar action before you try to access the request.)

assert_match /zombies or pirates/, @request[:thoughts_on]
response

Used to access the last response returned by the tested controller action. (Just like request, make sure to make a get or similar action beforehand.)

assert_match /I like turtles/, @response.body

Functional test-specific assertions you can use include

assert_response(type, [message])

Asserts that the response was a success, redirect, missing, or error:

assert_response :success  # Status code was 200
assert_response :redirect # Status code was in the 300-399 range
assert_response :missing  # Status code was 404
assert_response :error    # Status code was in the 500-599 range

You can also pass an explicit status number

# The response should be 401 (unauthorized)
assert_response(401)

or its symbolic equivalent:

assert_response(:unauthorized)

See the Status Codes table in the Appendix or ActionController::StatusCodes for a full list.

assert_redirected_to(options, [message])

Asserts that the tested controller action redirects as expected:

assert_redirected_to :controller => "articles", :action => "publish"

Also takes named routes:

assert_redirected_to publish_articles_url

Test all controller actions – just an assert_response :success ensures your actions aren’t flat-out broken, and is a great way to pick off low-hanging fruit when it comes to bugs.

View Tests

Hidden away in functional tests, view testing centers around a select few assertions that check to see if your templates have the HTML or CSS you’re looking for:

assert_select(selector, [equality_test], [message]), assert_select(element, selector, [equality_test], [message])

This is the most complex of all assertions.

assert_select selects some elements and runs an equality test on them. Without a given element (i.e. the second form), assert_select defaults to just looking at the response body.

The selector can be a CSS selector expression (in a String),

# At least one form exists
assert_select "form"

# A ul element with a menu-class applied exists under a div
# with a header-class applied to it
assert_select "body div.header ul.menu"

an expression with substitution values,

# At least one li with id like #item-1 or other number exists under an ol
assert_select "ol>li#?", /item-\d+/

or an HTML::Selector object.

# Select a form with class login and action /login
selector = HTML::Selector.new "form.login[action=/login]"
assert_select selector

When called with a block, assert_select passes an Array of selected elements to the block. Calling assert_select from the block with no element specified runs the assertion on the complete set of elements selected by the enclosing assertion. Alternatively, the Array may be iterated through so that assert_select can be called separately for each element.

# Any ol element with 4 li elements beneath it
assert_select "ol" do |elements|
  elements.each do |element|
    assert_select element, "li", 4
  end
end

# Select any ol with 8 li elements beneath it
assert_select "ol" do
  assert_select "li", 8
end

# All input fields in the form have a name
assert_select "form input" do
  assert_select "[name=?]", /.+/  # Not empty
end

The equality tests are as follows:

Table 8.1. Equality Tests

trueAt least 1 element selected (the default; used when no test specified)
# Page contains at least 1 form
assert_select "form", true
falseNo elements selected
# Page contains no forms
assert_select "form", false
String/RegexpText value of at least 1 element matches the String or regex
# Page title is "Welcome"
assert_select "title", "Welcome"

# Page title is "Hi Edward" or "Hi John"
assert_select "title", /Welcome (Edward|John)/
IntegerExactly this many elements are selected
# Form element includes 4 input fields
assert_select "form input", 4
Range[This..many] elements are selected
# Form element includes between 4 and 8 input fields
assert_select "form input", (4..8)


Narrow-down an equality test by passing in a Hash instead, using a combination of these key/value combinations to change the way elements are selected and equality tests are applied:

Table 8.2. Augmented Equality Tests

:key:value{:key => :value} Explanation
:textString/RegexpSelect just the elements that have this as their text value
:htmlString/RegexpSelect just the elements that have as their HTML content
:countIntegerTest passes if exactly this many elements are selected
:minimumIntegerAt least this many elements are selected
:maximumIntegerAt most this many elements are selected


# Page title is "Welcome" and there is only one title element
assert_select "title", {:count=>1, :text=>"Welcome"},
              "Wrong title or more than one title element"

For more on selectors, see the Rails documentation on HTML::Selector.

assert_select_email

The same as assert_select, but runs on extracted email. Note that you must enable email deliveries for it to work by setting ActionMailer::Base.perform_deliveries = true

assert_select_email do
  assert_select "h1", "Email alert"
end

assert_select_email do
  items = assert_select "ol>li"
  items.each do
     # Work with items here...
  end
end

As a general piece of advice with regard to testing your view code (to be taken with a grain of salt), it’s a good idea to assert the presence of only the really important; checking for the non-essentials (e.g. asserting an element’s colour is red) can slow development down when your visual style changes.

View Helper Tests

View helper tests look just like model unit tests, but inherit from ActionView::TestCase and live in test/unit/helpers.

Here’s a view helper:

module ArticlesHelper
  def author_byline(article)
    "This fine article penned by #{article.author.login}"
  end
end

and here’s its matching test:

require 'test_helper'

class ArticlesHelperTest < ActionView::TestCase
  def test_author_byline
    user = User.create(:login => 'Wilfred Owen')
    article = user.articles.create
    
    assert_equal 'This fine article penned by Wilfred Owen', author_byline(article)
  end
end

Note

Need to simulate a controller-set instance variable like @user ? Just set it in the test and it’ll be accessible to the helper being tested.

Routing Tests

Since the Rails framework allows you to test anything and everything in your application, it shouldn't be surprising that you can also test your application's routes. Learning to test your routes will allow you to better understand how the routing system works. Additionally, writing tests to generate and verify routes is a way to design your application's routes without having to make HTTP requests to your application over and over again. For complete coverage of routing in your application you can review the Chapter 6, Routing section.

There are three helper methods provided by Rails for testing your routes. These helpers are available in your Rails functional tests. The following examples all assume that the following routes are defined in the application's config/routes.rb file:

controller :articles do
  get 'articles/:id', :to => :show
  put 'articles/:id', :to => :update
end
assert_recognizes(expected_options, path, extras = {}, message = nil)

Asserts that path is a recognized route for the application and that the parsed route generates the hash expected_options. This helper is what you use to make sure that a route you've defined is matched by a particular path.

test "path should match route and parse the correct params" do
  expected_options = {
    :controller => 'articles',
    :action => 'show',
    :id => '50-world-cup-final'
  }
  assert_recognizes expected_options, 'articles/50-world-cup-final'
end

If you need to assert that any additional query parameters are correctly parsed into the expected_options hash then you can also pass the extras hash to assert_recognizes. The following is matching the path /articles/50-world-cup-final?view=print:

test "path should match route and parse the correct params with extras" do
  expected_options = {
    :controller => 'articles',
    :action => 'show',
    :id => '50-world-cup-final',
    :view => 'print'
  }
  assert_recognizes expected_options, 'articles/50-world-cup-final', :view => 'print'
end

Some routes are only matched when a particular HTTP verb is used. Instead of passing path as a string you can pass a hash containing :path and :method keys. Pass the appropriate HTTP verb as the value of the :method key. So, in order to match the route for updating articles, :method => :put is passed in with the path:

test "path should match route and parse the correct params with method" do
  expected_options = {
    :controller => 'articles',
    :action => 'update',
    :id => '50-world-cup-final'
  }
  
  assert_recognizes expected_options, :path => 'articles/50-world-cup-final', :method => :put
end
assert_generates(expected_path, options, defaults = {}, extras = {}, message = nil)

Asserts that the provided hash options generates the path expected_path.

test "routing options should generate the correct rewritten path" do
  options = {
    :controller => 'articles',
    :action => 'show',
    :id => '50-world-cup-final'
  }
  
  assert_generates 'articles/50-world-cup-final', options
end

The hash defaults can contain the default options for a particular controller and action, such as { :controller => 'articles', :action => :show }, however, you can also just put the defaults into the options has and the assertion will still be successful.

assert_routing(path, options, defaults = {}, extras = {}, message = nil)

A combination of assert_recognizes and assert_generates. Asserts that the hash options generates path and that path is recognized by the application and parses to the hash options.

test "path should generate correct rewritten path and parse to correct options" do
  options = {
    :controller => 'articles',
    :action => 'show',
    :id => '50-world-cup-final'
  }
  
  assert_routing 'articles/50-world-cup-final', options
end

As you can see, you get the most bang for your buck by using the assert_routing helper, as it does the work of both assert_recognizes and assert_generates for you.

Just like the test helpers that come with Test::Unit, all of the routing helpers take a final argument named message that allows you to set a custom failure message when the assertion fails.

Integration Tests

Integration tests run against the entire Rails stack, allowing you to test multiple controllers and actions, effectively simulating a user clicking through your application. They are the highest level of testing that Rails provides; they allow you to test all the components of your application, from routing to database access.

Creating an integration test

Unlike unit and functional tests, integration tests are not created automatically when you generate models and controllers. They are too tightly coupled to your application's individual workflow for Rails to be able to guess your needs. You can generate an integration test with the generate integration_test command:

$ rails generate integration_test Articles

Integration testing commands

A basic integration test looks a lot like a functional test, and uses the familar get, post, put, and delete HTTP methods to simulate the requests that would be created by a web browser. All the assertions you find in unit and functional tests are available for your testing needs, but Rails provides a few integration-specific helpers:

redirect?

True when the last response was a redirect.

follow_redirect!

Follows a single redirect. Raises an error if the last response was not a redirect.

get_via_redirect(obj, [parameters = nil], [headers = nil]), post_via_redirect(obj, [parameters = nil], [headers = nil]), put_via_redirect(obj, [parameters = nil], [headers = nil]), delete_via_redirect(obj, [parameters = nil], [headers = nil])

Acts just like the normal get, post, put, and delete methods but also follows any resulting redirects.

https?

True if the session is set to mimic an HTTPS request.

https!([flag = true])

Tells the test environment to mimic secure HTTPS requests (or a regular HTTP request if passed false).

host!(name)

Sets the host name for future test requests. Especially useful for testing apps with that provide custom sub-domains.

reset!

Resets the test instance, clearing any information in the session, as well as host and https settings.

Testing the article workflow

Integration tests look an awful lot like several functional tests put together. Here's a integration test that tests our entire article workflow, from sign-in, through creation, to deletion.

require 'test_helper'

class ArticlesTest < ActionController::IntegrationTest
  
  test "articles workflow" do
    alice = users(:alice)
    
    # login as alice
    post "/session", :user => {:login => alice.login,
                               :password => alice.password}
    follow_redirect!
    assert_response :success
    assert_equal "/articles", path
    
    # create an article
    post "/articles", :article => {:title => 'Brand new article'}
    assert_redirected_to "/articles"
    follow_redirect!
    assert_response :success
    
    # view the article
    article = alice.articles.first
    get "/articles/#{article.id}"
    assert_response :success
    
    # delete the article
    delete "/articles/#{article.id}"
    assert_redirected_to "/articles"
    follow_redirect!
    assert_response :success
    
    # expect 404 when trying to view a deleted article
    get "/articles/#{article.id}"
    assert_response :missing
  end
  
end

Testing multiple sessions

The interaction of multiple users with an application can get very complicated, and is often the source of hidden bugs. Testing for these bugs by juggling multiple browsers is clumsy and difficult. Thankfully, integration tests are much more flexible and can save us from these elusive bugs by allowing for multiple simultaneous sessions to be played with during the test.

Integration tests will automatically create a default session for you, but you can use the open_session method to create as many sessions as you need. Each session is extended with the full set of assertions and test helpers, so you can use helpers like assert_response and follow_redirect! and Rails will automatically use the appropriate response.

This example uses two sessions to prove that users may only delete their own posts:

require 'test_helper'

class ArticleSafetyTest < ActionController::IntegrationTest
  
  test "article safety" do
    # alice logs in
    alice = open_session
    alice.post "/session", :user => {
      :login => users(:alice).login,
      :password => users(:alice).password
    }
    alice.follow_redirect!
    alice.assert_response :success
    alice.assert_equal "/articles", alice.path
    
    # bob logs in
    bob = open_session
    bob.post "/session", :user => {
      :login => users(:bob).login,
      :password => users(:bob).password
    }
    bob.follow_redirect!
    bob.assert_response :success
    bob.assert_equal "/articles", bob.path
    
    # alice creates an article
    alice.post "/articles", :article => {
      :title => 'Brand new article'
    }
    article = alice.assigns(:article)
    alice.follow_redirect!
    alice.assert_response :success
    alice.assert_equal "/articles", alice.path
    
    # bob can't delete alice's article
    bob.delete "/articles/#{article.id}"
    bob.assert_response :not_found
    
    # alice can delete her article
    alice.delete "/articles/#{article.id}"
    alice.follow_redirect!
    alice.assert_response :success
    alice.assert_equal "/articles", alice.path
  end
  
end

Extending integration tests with helpers

Integration testing for complex interactions like those involving multiple users can get very verbose. Thankfully, we can decorate our sessions with custom helpers tailored to our application. With this technique, you can DRY up your tests while building a powerful and easy to read testing DSL for your app.

The first step is to define a module in test/test_helper.rb to encapsulate your reusable helpers and assertions. This example encapsulates three common actions in our app: logging in, viewing a specific article, and creating a new article:

module IntegrationHelper
  def login(name, password)
    user = users(name)
    post "/session", :user => {
      :login => name,
      :password => password
    }
    follow_redirect!
    assert_response :success
    assert_equal "/articles", path
  end
  
  def get_article(article)
    get "/articles/#{article.to_param}"
    assert_response :success
  end
  
  def post_article(attributes = {})
    post "/articles", :article => attributes
    article = assigns(:article)
    follow_redirect!
    assert_response :success
    assert_equal "/articles", path
    article
  end
end

The next step on our quest for DRY integration tests is create a helper so we can easily create new sessions. This helper loads the user from the users fixture, creates a new session (using the alternative block syntax), extends the session with our custom helpers, and finally logs in to the app, allowing us to log in new users with one simple, readable line of code:

def login_as(name)
  user = users(name)
  open_session do |session|
    session.extend IntegrationHelper
    session.login(user.login, user.password)
  end
end

With all the helpers finally in place, our multi-user integration tests are a thing of beauty—easy to read and even easier to write.

test "multi-user article posting" do
  # alice & bob log in
  alice = login_as(:alice)
  bob = login_as(:bob)
  
  # alice & bob create a articles
  alice_article = alice.post_article({
    :title => 'Alice is awesome'
  })
  bob_article = bob.post_article({
    :title => "Bob's the best"
  })
  
  # alice & bob can view each others articles
  alice.get_article(alice_article)
  alice.get_article(bob_article)
  
  bob.get_article(bob_article)
  bob.get_article(alice_article)
end

We’ve boiled down this complex, multi-user interaction into a concise story that tests the entire Rails stack—from router to database. These techniques allow you to build rock-solid apps without creating an impenetrable snarl of testing code.

Stubbing & Mocking

Some behaviors are particularly difficult, dangerous, or slow to test. If you're developing a SaaS application, charging a credit card with every run of your test suite would get expensive fast. If your social networking app interfaces with Twitter, Flickr, and Facebook, hitting all those APIs could take forever, not to mention fail every time your wifi goes out. What's a test-driven developer to do?

Mocks and stubs let you short-cut objects and methods, avoiding the dangerous or slow behavior, while confirming that your code still executes as expected.

Note

If you look at the tests behind Rails itself, you'll see all the mocking and stubbing uses the Mocha gem. Not surprisingly, Mocha has a strong foothold among Rails developers. This isn't the only way to do mocking, but the basic concepts presented here should be provided by any mocking/stubbing library.

Stubbing a method

This code posts a link to our twitter account every time we create a new article. Great marketing, but we can't junk up our Twitter stream every time we run our tests.

after_create :post_to_twitter
def post_to_twitter
  user = 'rubynews'
  pass = 'p@ssw*rd'
  msg  = "Checkout our new article, #{title} http://rubynews.host/#{stub}"

  Tweeter.update(user, pass, msg)
end

With Mocha, we can start to write a test that will intercept calls to Tweeter.update method, and prevent our code from actually contacting Twitter.

test "posts to twitter after create" do
  Tweeter.stubs(:update)  # do not actually run the update method
  Article.create(:title => 'Top 500 Ruby Gems')
end

Unfortunately, this code isn't actually testing anything. Mocha lets us define assertions in the form of expectations.

test "posts to twitter after create" do
  Tweeter.expects(:update)  # do not actually run the update method
  Article.create(:title => 'Top 500 Ruby Gems')
end

We can improve the test by defining the paramaters that Tweeter.update expects to be called with:

test "posts to twitter after create" do
  Tweeter.expects(:update).with('rubynews', 'p@ssw*rd', "Checkout our new article, Top 500 Ruby Gems http://rubynews.host/top500")
  Article.create(:title => 'Top 500 Ruby Gems', :stub => 'top500')
end

Stub & Mock Objects

Sometimes you need to stub out entire objects, instead of individual methods. Consider this method on the Article model which looks for tweets that link to our article.

def related_tweets
  # find tweets that link to this article
  tweets = Tweeter.search("http://rubynews.host/#{stub}")

  # only show other users' tweets
  tweets.reject do |tweet|
    tweet.from_user == 'rubynews'
  end
end

When writing our test, we first need to stub out the search method, so that we don't actually contact twitter.

test "fetchs related tweets" do
  query   = 'http://rubynews.host/articles/top500'
  results = []
  Tweeter.expects(:search).with(query).returns(results)
  assert_equal results, article(:rubynews).related_tweets
end

This is a good start, but by returning an empty array, we're not actually testing the code that filters by username. Instead, we can return stub objects.

test "fetchs related tweets" do
  our_tweet        = stub(:from_user => 'rubynews')
  other_tweet      = stub(:from_user => 'dhh')
  raw_results      = [our_tweet, other_tweet]
  filtered_results = [other_tweet]
  Tweeter.expects(:search).returns(raw_results)
  assert_equal filtered_results, article(:rubynews).related_tweets
end

The stub method takes a hash, each key becomes a method, and each value is its return value. Mocha also lets you set up mocks. Mocks are just like stubs, except they set up an expectation on each method. Our code with mocks ends up looking like:

test "fetchs related tweets" do
  our_tweet        = mock(:from_user => 'rubynews')
  other_tweet      = mock(:from_user => 'dhh')
  raw_results      = [our_tweet, other_tweet]
  filtered_results = [other_tweet]
  Tweeter.expects(:search).returns(raw_results)
  assert_equal filtered_results, articles(:rubynews).related_tweets
end

The only difference between these two versions is that the this one has assertions that will fail if the username method is never called on the mocks.

Note

Mocks and stubs are fantastic tools, but they can hide bugs that will bring your app crashing down in production. If Twitter changed its API, or an update to the Tweeter library contained a bug, our tests would continue to pass, but our application would be completely broken. Always be conscious of exactly what your tests are testing.

Running tests

Using rake

To run all tests, run

$ rake test

Individual test suite files can also be run manually:

$ ruby -I test test/unit/a_model_test.rb

Note

When running tests manually like this, you might first have to clone the testing database so that it has the same schema as your development environment’s (this database cloning step is done for you when using the rake commands):

$ rake db:test:clone

Related rake tasks can be discovered with $ rake -T test:

$ rake -T test
rake db:test:clone            # Recreate the test database from the current environment's database schema
rake db:test:clone_structure  # Recreate the test databases from the development structure
rake db:test:load             # Recreate the test database from the current schema.rb
rake db:test:prepare          # Check for pending migrations and load the test schema
rake db:test:purge            # Empty the test database
rake test                     # Run all unit, functional and integration tests
rake test:benchmark           # Run tests for benchmarktest:prepare / Benchmark the performance tests
rake test:functionals         # Run tests for functionalstest:prepare / Run the functional tests in test/functional
rake test:integration         # Run tests for integrationtest:prepare / Run the integration tests in test/integration
rake test:plugins             # Run tests for pluginsenvironment / Run the plugin tests in vendor/plugins/*/**/test (or specify with PLUGIN=name)
rake test:profile             # Run tests for profiletest:prepare / Profile the performance tests
rake test:recent              # Run tests for recenttest:prepare / Test recent changes
rake test:uncommitted         # Run tests for uncommittedtest:prepare / Test changes since last checkin (only Subversion and Git)
rake test:units               # Run tests for unitstest:prepare / Run the unit tests in test/unit

Running tests in a continuous feedback loop where the tests re-run themselves upon modification of test or application code can be accomplished by using the autotest -f command, part of Ryan Davis’ zentest gem. This facilitates staying in that zen-like state of flow, where time flies around you, and you’re rarely stuck on a hard-to-pin-down bug, or wondering what next to do.

Test environment

The testing environment configuration (which lives in config/environments/test.rb) makes things behave a little differently than development or production:

While it also uses its own database, the testing environment has each test run in its own transaction. This way, each test has a fresh version of the database and its loaded fixture data.

Note

If the validity of a test relies on the behaviour of transactions in your application, you might run into surprises. See how to temporarily turn off the transactional test behaviour in the ActiveRecord chapter.

The default testing environment also sets config.action_mailer.delivery_method to :test, which changes the way ActionMailer behaves; email is instead delivered to ActionMailer::Base.deliveries (an Array). See more about this in the ActionMailer chapter.

The testing environment also disables caching and a few other little things. Be sure to skim through the well-documented config/environments/test.rb file for more details.

Debugging within a test

A quick path to fixing bugs is to debug from within or in conjunction with writing and running your tests. The tests act as a clean way of recreating your reproduction steps, allowing you to throw in a debug statement in the middle of your test code, or in the code being tested. See the debugging chapter for more.

Site last updated on: December 11, 2012 at 10:21:28 PM PST
Cover for Rails 3 in a Nutshell

View 1 comment

  1. kira_corina – Posted Dec. 10, 2010

    I like how this chapter is organized, very clean and simple. Many thanks!

Add a comment

View 1 comment

  1. tordans – Posted Oct. 25, 2009

    I am new to test completely at this point. So this might be a stupid question: But I dont quite get why I should use those kind of tests in my app. The two excamples you show below (times_read + title) are much code for some very basic stuff that I might have found with a pagereload easily. So maybe some textual excamples of reallive problems solved by such test? Thanks

Add a comment

View 1 comment

  1. WilliamLang – Posted Nov. 5, 2010

    If its possible emphasize that assertions after a failed assertion in a test are not executed.

Add a comment

View 1 comment

  1. cube – Posted Nov. 5, 2009

    Specify Rails version the style has changed.

Add a comment

View 2 comments

  1. tordans – Posted Oct. 25, 2009

    Why now with () and no () before?

  2. gluis – Posted Feb. 16, 2012

    in ruby, parens are usually optional for method calls. e.g.

    assert_match /something/, @article.contents

    assert_match(/something/, @article.contents)

    for example, puts is a method call:

    puts "whatever you like"

    puts("whatever you like")

    Edited on February 16, 2012, 11:19 a.m. PST

Add a comment

View 1 comment

  1. davesailer – Posted Nov. 10, 2009

    Note that any attributes not provides in the fixture ==> Note that any attributes not provided in the fixture

    as in: provides ==> provided

Add a comment

View 1 comment

  1. tordans – Posted Oct. 25, 2009

    Instead...? Article.find(:ruby_wins) ??

Add a comment

View 1 comment

  1. WilliamLang – Posted Nov. 5, 2010

    I lol'd.

Add a comment

View 1 comment

  1. WilliamLang – Posted Nov. 5, 2010

    I thought using symbols for actions was the Rails way?

    get :index get :show, :id => @article.id

    etc

Add a comment

View 1 comment

  1. cube – Posted Nov. 5, 2009

    First of all, it's confusing because you described @request and @response above. And in this paragraph you incorrectly describe them as methods

    Testing internal controller state is generally done using the assertions mentioned previously in Unit Testing, and the following methods and special assertions whereas in code samples they are used as instance variables

Add a comment

View 1 comment

  1. kira_corina – Posted Dec. 10, 2010

    Missing eol above in: "Valid positions you can specify are: :top, :bottom, :before, and :after Just like its cousin, ... "

Add a comment

View 1 comment

  1. davesailer – Posted Nov. 10, 2009

    Awkward: "The next step on our quest for DRY integration tests is create a helper"

    Better: "The next step on our quest for DRY integration tests is to create a helper "

    Note the change to "is to create"

Add a comment

View 1 comment

  1. davesailer – Posted Nov. 10, 2009

    Picky, but "# alice & bob create a articles"

    should be "# alice & bob create articles"

    I.e., remove the " a " before "articles"

Add a comment