- •Contents
- •Preface to the Second Edition
- •Introduction
- •Rails Is Agile
- •Finding Your Way Around
- •Acknowledgments
- •Getting Started
- •The Architecture of Rails Applications
- •Models, Views, and Controllers
- •Active Record: Rails Model Support
- •Action Pack: The View and Controller
- •Installing Rails
- •Your Shopping List
- •Installing on Windows
- •Installing on Mac OS X
- •Installing on Linux
- •Development Environments
- •Rails and Databases
- •Rails and ISPs
- •Creating a New Application
- •Hello, Rails!
- •Linking Pages Together
- •What We Just Did
- •Building an Application
- •The Depot Application
- •Incremental Development
- •What Depot Does
- •Task A: Product Maintenance
- •Iteration A1: Get Something Running
- •Iteration A2: Add a Missing Column
- •Iteration A3: Validate!
- •Iteration A4: Prettier Listings
- •Task B: Catalog Display
- •Iteration B1: Create the Catalog Listing
- •Iteration B4: Linking to the Cart
- •Task C: Cart Creation
- •Sessions
- •Iteration C1: Creating a Cart
- •Iteration C2: A Smarter Cart
- •Iteration C3: Handling Errors
- •Iteration C4: Finishing the Cart
- •Task D: Add a Dash of AJAX
- •Iteration D1: Moving the Cart
- •Iteration D3: Highlighting Changes
- •Iteration D4: Hide an Empty Cart
- •Iteration D5: Degrading If Javascript Is Disabled
- •What We Just Did
- •Task E: Check Out!
- •Iteration E1: Capturing an Order
- •Task F: Administration
- •Iteration F1: Adding Users
- •Iteration F2: Logging In
- •Iteration F3: Limiting Access
- •Iteration F4: A Sidebar, More Administration
- •Task G: One Last Wafer-Thin Change
- •Generating the XML Feed
- •Finishing Up
- •Task T: Testing
- •Tests Baked Right In
- •Unit Testing of Models
- •Functional Testing of Controllers
- •Integration Testing of Applications
- •Performance Testing
- •Using Mock Objects
- •The Rails Framework
- •Rails in Depth
- •Directory Structure
- •Naming Conventions
- •Logging in Rails
- •Debugging Hints
- •Active Support
- •Generally Available Extensions
- •Enumerations and Arrays
- •String Extensions
- •Extensions to Numbers
- •Time and Date Extensions
- •An Extension to Ruby Symbols
- •with_options
- •Unicode Support
- •Migrations
- •Creating and Running Migrations
- •Anatomy of a Migration
- •Managing Tables
- •Data Migrations
- •Advanced Migrations
- •When Migrations Go Bad
- •Schema Manipulation Outside Migrations
- •Managing Migrations
- •Tables and Classes
- •Columns and Attributes
- •Primary Keys and IDs
- •Connecting to the Database
- •Aggregation and Structured Data
- •Miscellany
- •Creating Foreign Keys
- •Specifying Relationships in Models
- •belongs_to and has_xxx Declarations
- •Joining to Multiple Tables
- •Acts As
- •When Things Get Saved
- •Preloading Child Rows
- •Counters
- •Validation
- •Callbacks
- •Advanced Attributes
- •Transactions
- •Action Controller: Routing and URLs
- •The Basics
- •Routing Requests
- •Action Controller and Rails
- •Action Methods
- •Cookies and Sessions
- •Caching, Part One
- •The Problem with GET Requests
- •Action View
- •Templates
- •Using Helpers
- •How Forms Work
- •Forms That Wrap Model Objects
- •Custom Form Builders
- •Working with Nonmodel Fields
- •Uploading Files to Rails Applications
- •Layouts and Components
- •Caching, Part Two
- •Adding New Templating Systems
- •Prototype
- •Script.aculo.us
- •RJS Templates
- •Conclusion
- •Action Mailer
- •Web Services on Rails
- •Dispatching Modes
- •Using Alternate Dispatching
- •Method Invocation Interception
- •Testing Web Services
- •Protocol Clients
- •Secure and Deploy Your Application
- •Securing Your Rails Application
- •SQL Injection
- •Creating Records Directly from Form Parameters
- •Avoid Session Fixation Attacks
- •File Uploads
- •Use SSL to Transmit Sensitive Information
- •Knowing That It Works
- •Deployment and Production
- •Starting Early
- •How a Production Server Works
- •Repeatable Deployments with Capistrano
- •Setting Up a Deployment Environment
- •Checking Up on a Deployed Application
- •Production Application Chores
- •Moving On to Launch and Beyond
- •Appendices
- •Introduction to Ruby
- •Classes
- •Source Code
- •Resources
- •Index
- •Symbols
INTEGRATION TESTING OF APPLICATIONS 212
Addition Assertions
As well as assert_select, Rails provides similar selector-based assertions for validating the HTML content of RJS update and insert operations (assert_select_rjs), the encoded HTML within an XML response (assert_selected_encoded), and the HTML body of an e-mail (assert_select_email). Have a look at the Rails documentation for details.
13.4Integration Testing of Applications
The next level of testing is to exercise the flow through our application. In many ways, this is like testing one of the stories that our customer gave us when we first started to code the application. For example, we might have been told: A user goes to the store index page. They select a product, adding it to their cart. They then check out, filling in their details on the checkout form. When they submit, an order is created in the database containing their information, along with a single line item corresponding to the product they added to their cart.
This is ideal material for an integration test. Integration tests simulate a continuous session between one or more virtual users and our application. You can use them to send in requests, monitor responses, follow redirects, and so on.
When you create a model or controller, Rails creates the corresponding unit and functional tests. Integration tests are not automatically created, however, so you’ll need to use a generator to create one.
depot> ruby script/generate integration_test user_stories exists test/integration/
create test/integration/user_stories_test.rb
Notice that Rails automatically adds _test to the name of the test.
Let’s look at the generated file.
require "#{File.dirname(__FILE__)}/../test_helper"
class UserStoriesTest < ActionController::IntegrationTest
#fixtures :your, :models
#Replace this with your real tests. def test_truth
assert true end
end
This looks a bit like a functional test, but our test class inherits from IntegrationTest.
Report erratum
INTEGRATION TESTING OF APPLICATIONS 213
Let’s launch straight in and implement the test of our story. Because we’ll be buying something, we’ll need our products fixture, so we load it at the top of the class.
fixtures :products
Just as with unit and functional tests, our test will be written in a method whose name starts test_.
def test_buying_a_product
# ...
end
By the end of the test, we know we’ll want to have added an order to the orders table and a line item to the line_items table, so let’s empty them out before we start. And, because we’ll be using the Ruby book fixture data a lot, let’s load it into a local variable.
Download depot_r/test/integration/user_stories_test.rb
LineItem.delete_all Order.delete_all
ruby_book = products(:ruby_book)
Let’s attack the first sentence in the user story: A user goes to the store index page.
Download depot_r/test/integration/user_stories_test.rb
get "/store/index" assert_response :success assert_template "index"
This almost looks like a functional test. The main difference is the get method: in a functional test we check just one controller, so we specify just an action when calling get. In an integration test, however, we can wander all over the application, so we need to pass in a full (relative) URL for the controller and action to be invoked.
The next sentence in the story goes They select a product, adding it to their cart.
We know that our application uses an AJAX request to add things to the cart, so we’ll use the xml_http_request method to invoke the action. When it returns, we’ll check that the cart now contains the requested product.
Download depot_r/test/integration/user_stories_test.rb
xml_http_request "/store/add_to_cart" , :id => ruby_book.id assert_response :success
cart = session[:cart] assert_equal 1, cart.items.size
assert_equal ruby_book, cart.items[0].product
Report erratum
INTEGRATION TESTING OF APPLICATIONS 214
In a thrilling plot twist, the user story continues, They then check out.... That’s easy in our test.
Download depot_r/test/integration/user_stories_test.rb
post "/store/checkout" assert_response :success assert_template "checkout"
At this point, the user has to fill in their details on the checkout form. Once they do, and they post the data, our application creates the order and redirects to the index page. Let’s start with the HTTP side of the world by posting the form data to the save_order action and verifying we’ve been redirected to the index. We’ll also check that the cart is now empty. The test helper method post_via_redirect generates the post request and then follows any redirects returned until a regular 200 response is returned.
Download depot_r/test/integration/user_stories_test.rb |
|
post_via_redirect "/store/save_order" , |
|
:order => { :name |
=> "Dave Thomas", |
:address |
=> "123 The Street", |
=> "dave@pragprog.com", |
|
:pay_type |
=> "check" } |
assert_response :success assert_template "index"
assert_equal 0, session[:cart].items.size
Finally, we’ll wander into the database and make sure we’ve created an order and corresponding line item and that the details they contain are correct. Because we cleared out the orders table at the start of the test, we’ll simply verify that it now contains just our new order.
Download depot_r/test/integration/user_stories_test.rb
orders = Order.find(:all) assert_equal 1, orders.size order = orders[0]
assert_equal "Dave Thomas", |
order.name |
|
assert_equal "123 The Street", |
order.address |
|
assert_equal |
"dave@pragprog.com", order.email |
|
assert_equal |
"check", |
order.pay_type |
assert_equal 1, order.line_items.size line_item = order.line_items[0] assert_equal ruby_book, line_item.product
And that’s it. The following page shows the full source of the integration test.
Report erratum
INTEGRATION TESTING OF APPLICATIONS 215
Download depot_r/test/integration/user_stories_test.rb
require "#{File.dirname(__FILE__)}/../test_helper"
class UserStoriesTest < ActionController::IntegrationTest fixtures :products
#A user goes to the store index page. They select a product, adding
#it to their cart. They then check out, filling in their details on
#the checkout form. When they submit, an order is created in the
#database containing their information, along with a single line
#item corresponding to the product they added to their cart.
def test_buying_a_product LineItem.delete_all Order.delete_all
ruby_book = products(:ruby_book)
get "/store/index" assert_response :success assert_template "index"
xml_http_request "/store/add_to_cart" , :id => ruby_book.id assert_response :success
cart = session[:cart] assert_equal 1, cart.items.size
assert_equal ruby_book, cart.items[0].product
post "/store/checkout" |
|
|
|
assert_response |
:success |
|
|
assert_template |
"checkout" |
|
|
post_via_redirect "/store/save_order" , |
|
||
|
:order => { :name |
=> "Dave Thomas", |
|
|
:address |
=> "123 The Street", |
|
|
=> "dave@pragprog.com", |
||
|
:pay_type => "check" } |
||
assert_response |
:success |
|
|
assert_template |
"index" |
|
|
assert_equal 0, session[:cart].items.size |
|||
orders = Order.find(:all) |
|
|
|
assert_equal 1, orders.size |
|
|
|
order = orders[0] |
|
|
|
assert_equal "Dave Thomas", |
order.name |
||
assert_equal "123 The Street", |
order.address |
||
assert_equal "dave@pragprog.com", order.email |
|||
assert_equal "check", |
order.pay_type |
assert_equal 1, order.line_items.size line_item = order.line_items[0] assert_equal ruby_book, line_item.product
end end
Report erratum
INTEGRATION TESTING OF APPLICATIONS 216
Even Higher-Level Tests
(This section contains advanced material that can safely be skipped.)
The integration test facility is very nice: we know of no other framework that offers built-in testing at this high of a level. But we can take it even higher. Imagine being able to give your QA people a minilanguage (sometimes called a domain-specific language) for application testing. They could write our previous test with language like
Download depot_r/test/integration/dsl_user_stories_test.rb
def test_buying_a_product dave = regular_user dave.get "/store/index" dave.is_viewing "index" dave.buys_a @ruby_book
dave.has_a_cart_containing @ruby_book dave.checks_out DAVES_DETAILS dave.is_viewing "index"
check_for_order DAVES_DETAILS, @ruby_book end
This code uses a hash, DAVES_DETAILS, defined inside the test class.
Download depot_r/test/integration/dsl_user_stories_test.rb
DAVES_DETAILS |
= { |
:name |
=> "Dave Thomas", |
:address |
=> "123 The Street", |
=> "dave@pragprog.com", |
|
:pay_type |
=> "check" |
} |
|
It might not be great literature, but it’s still pretty readable. So, how do we provide them with this kind of functionality? It turns out to be fairly easy using a neat Ruby facility called singleton methods.
If obj is a variable containing any Ruby object, we can define a method that applies only to that object using the syntax
def obj.method_name
# ...
end
Once we’ve done this, we can call method_name on obj just like any other method.
obj.method_name
That’s how we’ll implement our testing language. We’ll create a new testing session using the open_session method and define all our helper methods on this session. In our example, this is done in the regular_user method.
Report erratum
INTEGRATION TESTING OF APPLICATIONS 217
Download depot_r/test/integration/dsl_user_stories_test.rb
def regular_user open_session do |user|
def user.is_viewing(page) assert_response :success assert_template page
end
def user.buys_a(product)
xml_http_request "/store/add_to_cart" , :id => product.id assert_response :success
end
def user.has_a_cart_containing(*products) cart = session[:cart]
assert_equal products.size, cart.items.size for item in cart.items
assert products.include?(item.product) end
end
def user.checks_out(details) post "/store/checkout" assert_response :success assert_template "checkout"
post_via_redirect "/store/save_order" ,
|
:order => { |
|
|
:name |
=> details[:name], |
|
:address |
=> details[:address], |
|
=> details[:email], |
|
|
:pay_type => details[:pay_type] |
|
|
} |
|
assert_response |
:success |
|
assert_template |
"index" |
|
assert_equal 0, session[:cart].items.size end
end end
The regular_user method returns this enhanced session object, and the rest of our script can then use it to run the tests.
Once we have this minilanguage defined, it’s easy to write more tests. For example, here’s a test that verifies that there’s no interaction between two users buying products at the same time. (We’ve indented the lines related to Mike’s session to make it easier to see the flow.)
Download depot_r/test/integration/dsl_user_stories_test.rb
def test_two_people_buying dave = regular_user
mike = regular_user
Report erratum
INTEGRATION TESTING OF APPLICATIONS 218
dave.buys_a @ruby_book mike.buys_a @rails_book
dave.has_a_cart_containing @ruby_book dave.checks_out DAVES_DETAILS
mike.has_a_cart_containing @rails_book check_for_order DAVES_DETAILS, @ruby_book
mike.checks_out MIKES_DETAILS check_for_order MIKES_DETAILS, @rails_book
end
We show the full listing of the minilanguage version of the testing class starting on page 673.
Integration Testing Support
Integration tests are deceptively similar to functional tests, and indeed all the same assertions we’ve used in unit and functional testing work in integration tests. However, some care is needed, because many of the helper methods are subtly different.
Integration tests revolve around the idea of a session. The session represents a user at a browser interacting with our application. Although similar in concept to the session variable in controllers, the word session here means something different.
When you start an integration test, you’re given a default session (you can get to it in the instance variable integration_session if you really need to). All of the integration test methods (such as get) are actually methods on this session: the test framework delegates these calls for you. However, you can also create explicit sessions (using the open_session method) and invoke these methods on it directly. This lets you simulate multiple users at the same time (or lets you create sessions with different characteristics to be used sequentially in your test). We saw an example of multiple sessions in the test on page 216.
Integration test sessions have the following attributes. Be careful to use an explicit receiver when assigning to them in an integration test.
self.accept = "text/plain" |
# works |
||
open_session do |
|sess| |
|
|
sess.accept = |
"text/plain" |
# |
works |
end |
|
|
|
accept = "text/plain" |
# |
doesn't work--local variable |
In the list that follows, sess stands for a session object.
accept
The accept header to send.
sess.accept = "text/xml,text/html"
Report erratum
INTEGRATION TESTING OF APPLICATIONS 219
controller
A reference to the controller instance used by the last request.
cookies
A hash of the cookies. Set entries in this hash to send cookies with a request, and read values from the hash to see what cookies were set in a response.
headers
The headers returned by the last response as a hash.
host
Set this value to the host name to be associated with the next request. Useful when you write applications whose behavior depends on the host name.
sess.host = "fred.blog_per_user.com"
path
The URI of the last request.
remote_addr
The IP address to be associated with the next request. Possibly useful if your application distinguishes between local and remote requests.
sess.remote_addr = "127.0.0.1"
request
The request object used by the last request.
response
The response object used by the last request.
status
The HTTP status code of the last request (200, 302, 404, and so on).
status_message
The status message that accompanied the status code of the last request (OK, Not Found, and so on).
Integration Testing Convenience Methods
The following methods can be used within integration tests.
follow_redirect!()
If the last request to a controller resulted in a redirect, follow it.
get(path, params=nil, headers=nil) post(path, params=nil, headers=nil)
xml_http_request(path, params=nil, headers=nil)
Performs a GET, POST, or XML_HTTP request with the given parameters.
Report erratum