Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Agile Web Development With Rails, 2nd Edition (2006).pdf
Скачиваний:
30
Добавлен:
17.08.2013
Размер:
6.23 Mб
Скачать

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",

:email

=> "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",

 

:email

=> "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",

:email

=> "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],

 

:email

=> 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