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:
Write a test or refactor an existing one
Run the test, watch it fail
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
endIts 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
endEach 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 errorsLooks 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 :articlesThe 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.rbtest/fixtures/articles.ymltest/functional/articles_controller_test.rbtest/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
booleanis true. Displaysmessageon failure or "booleanis not true."assert @article.popular?
assert_nil(obj, message = "")Passes when
objis 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
stringmatches the regular expressionpattern.assert_match(/edward/i, @article.author)
assert_no_match(pattern, string, message = "")Passes when
stringdoes not match the regular expressionpattern.assert_no_match(/bill/, @article.author)
assert_in_delta(expected_float, actual_float, delta, message = "")Passes when
expected_floatandactual_floatare no further apart thandelta.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
Stringor anArrayof expressions and passes when the value returned by the expression before and after the block changes bydifference. Note that if you leave out thedifferenceargument, 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
endWith 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
endFixtures
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
endYou 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
endNote 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:
@controllerThe controller the test is about
@requestThe request just made to the controller being tested (accessible only after a request has been issued though a
getcall or similar)@responseThe 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.bodyand@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').titlesessionUsed to access the
sessionin the tested controller action.# Asserts that there's a user id set in the session assert session['user_id']
flashUsed to access the
flashin the tested controller action.# Assert that the flash contains meaningful feedback assert_match /account updated/, flash['info']
cookiesUsed to access the
cookiesin the tested controller action.assert_equal expected_favourite_colour, cookies['favourite_colour']
requestUsed to access the last
requestmade in the tested controller action. (So be sure to have made agetor similar action before you try to access therequest.)assert_match /zombies or pirates/, @request[:thoughts_on]
responseUsed to access the last
responsereturned by the tested controller action. (Just likerequest, make sure to make agetor 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::StatusCodesfor 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_selectselects some elements and runs an equality test on them. Without a given element (i.e. the second form),assert_selectdefaults 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::Selectorobject.# 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_selectpasses anArrayof selected elements to the block. Callingassert_selectfrom the block with no element specified runs the assertion on the complete set of elements selected by the enclosing assertion. Alternatively, theArraymay be iterated through so thatassert_selectcan 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 endThe 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_emailThe same as
assert_select, but runs on extracted email. Note that you must enable email deliveries for it to work by settingActionMailer::Base.perform_deliveries = trueassert_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
endand 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
endNote
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
pathis a recognized route for the application and that the parsed route generates the hashexpected_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' endIf you need to assert that any additional query parameters are correctly parsed into the
expected_optionshash then you can also pass theextrashash toassert_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' endSome routes are only matched when a particular HTTP verb is used. Instead of passing
pathas a string you can pass a hash containing:pathand:methodkeys. Pass the appropriate HTTP verb as the value of the:methodkey. So, in order to match the route for updating articles,:method => :putis 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 endassert_generates(expected_path,options,defaults= {}, extras= {},message= nil)Asserts that the provided hash
optionsgenerates the pathexpected_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 endThe hash
defaultscan 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_recognizesandassert_generates. Asserts that the hashoptionsgeneratespathand thatpathis recognized by the application and parses to the hashoptions.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 endAs you can see, you get the most bang for your buck by using the
assert_routinghelper, as it does the work of bothassert_recognizesandassert_generatesfor 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, and
put 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:delete
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, andputmethods but also follows any resulting redirects.deletehttps?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
endTesting 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 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 open_session and assert_response and Rails will
automatically use the appropriate response.follow_redirect!
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
endExtending 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
endThe 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
endWith 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)
endWe’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)
endWith 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')
endStub & 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
endWhen 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.





View 1 comment




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