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.