• No results found

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

Report erratum

TEST-DRIVENDEVELOPMENT 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 SearchControllerwith asearch( ) action.

depot> ruby script/generate controller Search search

There’s no code in the generatedsearch( ) 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 insearch_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 notbefore we write production code. OK, first we need

to send a request to thesearch( ) 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 theresults.rhtmlview. 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

Report erratum

TEST-DRIVENDEVELOPMENT 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 thesearch( )

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 thesearch( ) 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

Report erratum

RUNNINGTESTS WITHRAKE 165

rendered by the view will still need some work and a keen eye, but we know we’re done with the underlying controllers and models when the functional test passes. And what about our customer? Well, seeing us write this test first makes her think she’d like us to try using tests as a specification again in the next iteration.

That’s just one revolution through the test-driven development cycle—

write an automated test before the code that makes it pass. For each

new feature that the customer requests, we’d go through the cycle again. And if a bug pops up (gasp!), we’d write a test to corner it and, when the test passed, we’d know the bug was cornered for life.

Done regularly, test-driven development not only helps you incrementally create a solid suite of regression tests but it also improves the quality of your design. Two for the price of one.