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

USING MOCK OBJECTS 161

When you’re debugging tests, it’s incredibly helpful to watch the log/test.log file. For functional tests, the log file gives you an end-to-end view inside of your application as it goes through the motions.

Phew, we quickly cranked out a few tests there. It’s not a very comprehensive suite of tests, but we learned enough to write tests until the cows come home. Should we drop everything and go write tests for a while? Well, we took the high road on most of these, so writing a few tests off the beaten path certainly wouldn’t hurt. At the same time, we need to be practical and write tests for those things that are most likely to break first. And with the help Rails offers, you’ll find that indeed you do have more time to test.

12.4 Using Mock Objects

At some point we’ll need to add code to the Depot application to actually collect payment from our dear customers. So imagine that we’ve filled out all the paperwork necessary to turn credit card numbers into real money in our bank account. Then we created a PaymentGateway class in the file app/models/payment_gateway.rb that communicates with a credit-card processing gateway. And we’ve wired up the Depot application to handle credit cards by adding the following code to the save_order( ) action of the

StoreController.

gateway = PaymentGateway.new

 

response = gateway.collect(:login

=> 'username',

:password

=> 'password',

:amount

=> cart.total_price,

:card_number

=> @order.card_number,

:expiration

=> @order.card_expiration,

:name

=> @order.name)

When the collect( ) method is called, the information is sent out over the network to the backend credit-card processing system. This is good for our pocketbook, but it’s bad for our functional test because the StoreController now depends on a network connection with a real, live credit-card processor on the other end. And even if we had both of those things available at all times, we still don’t want to send credit card transactions every time we run the functional tests.

Instead, we simply want to test against a mock, or replacement, PaymentGateway object. Using a mock frees the tests from needing a network connection and ensures more consistent results. Thankfully, Rails makes mocking objects a breeze.

Prepared exclusively for Rida Al Barazi

Report erratum

 

TEST -DRIVEN DEVELOPMENT

162

 

To mock out the collect( ) method in the testing environment, all we need

 

 

to do is create a payment_gateway.rb file in the test/mocks/test directory that

 

 

defines the methods we want to mock out. That is, mock files must have

 

 

the same filename as the model in the app/models directory they are replac-

 

 

ing. Here’s the mock file.

 

File 120

require 'models/payment_gateway'

 

 

class PaymentGateway

 

 

def collect(request)

 

 

# I'm a mocked out method

 

 

:success

 

 

end

 

 

end

 

 

Notice that the mock file actually loads the original PaymentGateway class

 

 

(using require( )) and then reopens it. That means we don’t have to mock out

 

 

all the methods of PaymentGateway, just the methods we want to redefine

 

 

for when the tests run. In this case, the collect( ) simply returns a fake

 

 

response.

 

 

With this file in place, the StoreController will use the mock PaymentGateway

 

 

class. This happens because Rails arranges the search path to include

 

 

the mock path first—test/mocks/test/payment_gateway.rb is loaded instead

 

 

of app/models/payment_gateway.rb.

 

 

That’s all there is to it. By using mocks, we can streamline the tests

 

 

and concentrate on testing what’s most important. And Rails makes it

 

 

painless.

 

12.5 Test-Driven Development

So far we’ve been writing unit and functional tests for code that already exists. Let’s turn that around for a minute. The customer stops by with a novel idea: allow Depot users to search for products. So, after sketching out the screen flow on paper for a few minutes, it’s time to lay down some code. We have a rough idea of how to implement the search feature, but some feedback along the way sure would help keep us on the right path.

That’s what test-driven development is all about. Instead of diving into the implementation, write a test first. Think of it as a specification for how you want the code to work. When the test passes, you know you’re done coding. Better yet, you’ve added one more test to the application.

Let’s give it a whirl with a functional test for searching. OK, so which controller should handle searching? Well, come to think of it, both buyers

Prepared exclusively for Rida Al Barazi

Report erratum

TEST -DRIVEN DEVELOPMENT

163

 

and sellers might want to search for products. So rather than adding

 

a search( ) action to store_controller.rb or admin_controller.rb, we generate a

 

SearchController with a search( ) action.

 

depot> ruby script/generate controller Search search

 

There’s no code in the generated search( ) method, but that’s OK because

 

we don’t really know how a search should work just yet. Let’s flush that

 

out with a test by cracking open the functional test that was generated for

 

us in search_controller_test.rb.

File 118

require File.dirname(__FILE__) + '/../test_helper'

 

require 'search_controller'

 

class SearchControllerTest < Test::Unit::TestCase

 

fixtures :products

 

def setup

 

@controller = SearchController.new

 

@request = ActionController::TestRequest.new

 

@response = ActionController::TestResponse.new

 

end

 

end

 

At this point, the customer leans a little closer. She’s never seen us write a

 

test, and certainly not before we write production code. OK, first we need

 

to send a request to the search( ) action, including the query string in the

 

request parameters. Something like this:

File 118

def test_search

 

get :search, :query => "version control"

 

assert_response :success

 

That should give us a flash notice saying it found one product because the

 

products fixture has only one product matching the search query. As well,

 

the flash notice should be rendered in the results.rhtml view. We continue to

 

write all that down in the test method.

File 118

assert_equal "Found 1 product(s).", flash[:notice]

 

assert_template "search/results"

 

Ah, but the view will need a @products instance variable set so that it

 

can list the products that were found. And in this case, there’s only one

 

product. We need to make sure it’s the right one.

File 118

products = assigns(:products)

 

assert_not_nil products

 

assert_equal 1, products.size

 

assert_equal "Pragmatic Version Control", products[0].title

 

We’re almost there. At this point, the view will have the search results. But

 

how should the results be displayed? On our pencil sketch, it’s similar

 

to the catalog listing, with each result laid out in subsequent rows. In

Prepared exclusively for Rida Al Barazi

Report erratum

 

TEST -DRIVEN DEVELOPMENT

164

 

fact, we’ll be using some of the same CSS as in the catalog views. This

 

 

particular search has one result, so we’ll generate HTML for exactly one

 

 

product. “Yes!”, we proclaim while pumping our fists in the air and making

 

 

our customer a bit nervous, “the test can even serve as a guide for laying

 

 

out the styled HTML!”

 

File 118

assert_tag :tag => "div",

 

 

:attributes => { :class => "results" },

 

 

:children => { :count => 1,

 

 

:only => { :tag => "div",

 

 

:attributes => { :class => "catalogentry" }}}

 

 

Here’s the final test.

 

File 118

def test_search

 

 

get :search, :query => "version control"

 

 

assert_response :success

 

 

assert_equal "Found 1 product(s).", flash[:notice]

 

 

assert_template "search/results"

 

 

products = assigns(:products)

 

 

assert_not_nil products

 

 

assert_equal 1, products.size

 

 

assert_equal "Pragmatic Version Control", products[0].title

 

 

assert_tag :tag => "div",

 

:attributes => { :class => "results" }, :children => { :count => 1,

:only => { :tag => "div",

:attributes => { :class => "catalogentry" }}}

end

Now that we’ve defined the expected behavior by writing a test, let’s try to run it.

depot> ruby test/functional/search_controller_test.rb

Loaded suite test/functional/search_controller_test

Started F

Finished in 0.273517 seconds. 1) Failure:

test_search(SearchControllerTest) [test/functional/search_controller_test.rb:23]: <"Found 1 product(s)."> expected but was <nil>.

1 tests, 2 assertions, 1 failures, 0 errors

Not surprisingly, the test fails. It expects that after requesting the search( ) action the view will have one product. But the search( ) action that Rails generated for us is empty, of course. All that remains now is to write the code for the search( ) action that makes the functional test pass. That’s left as an exercise for you, dear reader.

Why write a failing test first? Simply put, it gives us a measurable goal. The test tells us what’s important in terms of inputs, control flow, and outputs before we invest in a specific implementation. The user interface

Prepared exclusively for Rida Al Barazi

Report erratum