Rails organizes tests into unit, functional, and integration tests. Before ex- plaining integration tests, let’s have a brief recap of what we have covered so far:
Unit testing of models
Model classes contain business logic. For example, when we add a prod- uct to a cart, the cart model class checks to see whether that product is already in the cart’s list of items. If so, it increments the quantity of that item; if not, it adds a new item for that product.
ITERATIONH2: INTEGRATIONTESTING OFAPPLICATIONS 189 Functional testing of controllers
Controllers direct the show. They receive incoming web requests (typ- ically user input), interact with models to gather application state, and then respond by causing the appropriate view to display something to the user. So when we’re testing controllers, we’re making sure that a given request is answered with an appropriate response. We still need models, but we already have them covered with unit tests.
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 the following: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 informa- tion, along with a single line item corresponding to the product they added to their cart. Once the order has been received, an email is sent confirming their purchase.
This is ideal material for an integration test. Integration tests simulate a con- tinuous 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 or functional tests. Integration tests are not automatically created, however, but you can use a generator to create one.
depot> rails generate integration_test user_stories invoke test_unit
create test/integration/user_stories_test.rb
Notice that Rails automatically adds_testto the name of the test.
Let’s look at the generated file:
require 'test_helper'
class UserStoriesTest < ActionController::IntegrationTest fixtures :all
# Replace this with your real tests.
test "the truth" do assert true end
end
Let’s launch straight in and implement the test of our story. Because we’ll only be testing the purchase of a product, we’ll need only our products fixture.
Report erratum this copy is(P1.0 printing, March 2011)
Download from Wow! eBook <www.wowebook.com>
ITERATIONH2: INTEGRATIONTESTING OFAPPLICATIONS 190
So instead of loading all the fixtures, let’s load only this one:
fixtures :products
Now let’s build a test namedbuying a product. 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_itemstable, 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)
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 "/"
assert_response :success assert_template "index"
This almost looks like a functional test. The main difference is thegetmethod.
In a functional test, we check just one controller, so we specify just an action when callingget. 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 goesThey 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 thexml_http_requestmethod 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 :post, '/line_items', :product_id => ruby_book.id assert_response :success
cart = Cart.find(session[:cart_id]) assert_equal 1, cart.line_items.size
assert_equal ruby_book, cart.line_items[0].product
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
get "/orders/new"
assert_response :success assert_template "new"
ITERATIONH2: INTEGRATIONTESTING OFAPPLICATIONS 191 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 redi- rects to the index page. Let’s start with the HTTP side of the world by post- ing the form data to thesave_order action and verifying we’ve been redirected to the index. We’ll also check that the cart is now empty. The test helper methodpost_via_redirectgenerates the post request and then follows any redi- rects returned until a nonredirect response is returned.
Download depot_r/test/integration/user_stories_test.rb
post_via_redirect "/orders",
:order => { :name => "Dave Thomas", :address => "123 The Street", :email => "dave@example.com", :pay_type => "Check" }
assert_response :success assert_template "index"
cart = Cart.find(session[:cart_id]) assert_equal 0, cart.line_items.size
Next, 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 theorderstable 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.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@example.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
Finally, we’ll verify that the mail itself is correctly addressed and has the expected subject line:
Download depot_r/test/integration/user_stories_test.rb
mail = ActionMailer::Base.deliveries.last assert_equal ["dave@example.com"], mail.to
assert_equal 'Sam Ruby <depot@example.com>', mail[:from].value assert_equal "Pragmatic Store Order Confirmation", mail.subject
Report erratum this copy is(P1.0 printing, March 2011)
Download from Wow! eBook <www.wowebook.com>
ITERATIONH2: INTEGRATIONTESTING OFAPPLICATIONS 192 And that’s it. Here’s the full source of the integration test:
Download depot_r/test/integration/user_stories_test.rb
require 'test_helper'
class UserStoriesTest < ActionDispatch::IntegrationTest fixtures :products
# A user goes to the index page. They select a product, adding it to their
# cart, and check out, filling in their details on the checkout form. When
# they submit, an order is created containing their information, along with a
# single line item corresponding to the product they added to their cart.
test "buying a product" do LineItem.delete_all Order.delete_all
ruby_book = products(:ruby) get "/"
assert_response :success assert_template "index"
xml_http_request :post, '/line_items', :product_id => ruby_book.id assert_response :success
cart = Cart.find(session[:cart_id]) assert_equal 1, cart.line_items.size
assert_equal ruby_book, cart.line_items[0].product get "/orders/new"
assert_response :success assert_template "new"
post_via_redirect "/orders",
:order => { :name => "Dave Thomas", :address => "123 The Street", :email => "dave@example.com", :pay_type => "Check" }
assert_response :success assert_template "index"
cart = Cart.find(session[:cart_id]) assert_equal 0, cart.line_items.size orders = Order.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@example.com", order.email assert_equal "Check", order.pay_type assert_equal 1, order.line_items.size
ITERATIONH2: INTEGRATIONTESTING OFAPPLICATIONS 193
mail = ActionMailer::Base.deliveries.last assert_equal ["dave@example.com"], mail.to
assert_equal 'Sam Ruby <depot@example.com>', mail[:from].value assert_equal "Pragmatic Store Order Confirmation", mail.subject end
end
Taken together, unit, functional, and integration tests give you the flexibility to test aspects of your application either in isolation or in combination with each other. In Section26.5,Finding More at RailsPlugins.org, on page432, we will tell you where you can find add-ons that take this to the next level and allow you to write plain-text descriptions of behaviors that can be read by your customer and be verified automatically.
Speaking of our customer, it is time to wrap this iteration up and see what functionality is next in store for Depot.
What We Just Did
Without much code and with just a few templates, we have managed to pull off the following:
• We configured our development, test, and production environments for our Rails application to enable the sending of outbound emails.
• We created and tailored a mailer that will send confirmation emails in both plain-text and HTML formats to people who order our products.
• We created both a functional test for the emails produced, as well as an integration test that covers the entire order scenario.
Playtime
• Add aship_datecolumn to the orders table, and send a notification when this value is updated by theOrdersController.
• Update the application to send an email to the system administrator, namely, yourself, when there is an application failure such as the one we handled in Section10.2,Iteration E2: Handling Errors, on page131.
• Add integration tests for both of the previous items.
Report erratum this copy is(P1.0 printing, March 2011)
Download from Wow! eBook <www.wowebook.com>
In this chapter, we’ll see
• adding virtual attributes to models,
• using more validations,
• coding forms without underlying models,
• adding authentication to a session,
• usingrails console,
• using database transactions, and
• writing an Active Record hook.
Chapter 14