Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 55 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
55
Dung lượng
689,42 KB
Nội dung
TESTING CONTROLLERS 156 redirect_to_url The full URL that the previous action redirected to. assert_equal "http://test.host/login", redirect_to_url We’ll see more of these assertions and variables in action as we write more tests, so let’s get back to it. Buy Something Already! The next feature we’d be wise to test is that a user can actually place an order for a product. That means switching our perspective over to the storefront. We’ll walk through each action one step at a time. Listing Products for Sale Back in the StoreController,theindex( ) action puts all the salable products into the @products instance variable. It then renders the index.rhtml view, which uses the @products variable to list all the products for sale. To write a test for the index( ) action, we need some products. Thankfully, we already have two salable products in our products fixture. We just need to modify the store_controller_test.rb file to load the products fixture. While we’re at it, we load the orders fixture, which contains one order that we’ll need a bit later. File 119 require File.dirname(__FILE__) + '/ /test_helper' require 'store_controller' # Reraise errors caught by the controller. class StoreController; def rescue_action(e) raise e end; end class StoreControllerTest < Test::Unit::TestCase fixtures :products, :orders def setup @controller = StoreController.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new end def teardown LineItem.delete_all end end Notice that we’ve added a new method called teardown( ) to this test case. We do this because some of the test methods we’ll be writing will indirectly cause line items to be saved in the test database. If defined, the teardown() method is called after every test method. This is a handy way to clean up the test database so that the results of one test method don’t affect another. By calling LineItem.delete_all() in teardown(), the line_items table in the test database will be cleared after each test method runs. If we’re Report erratum TESTING CONTROLLERS 157 using explicit test fixtures, we don’t need to do this; the fixture takes care of deleting data for us. In this case, though, we’re adding line items but we aren’t using a line items fixture, so we have to tidy up manually. Then we add a test_index( ) method that requests the index( ) action and verifies that the store/index.rhtml view gets two salable products. File 119 def test_index get :index assert_response :success assert_equal 2, assigns(:products).size assert_template "store/index" end You may be thinking we have gratuitous overlap in testing here. It’s true, we already have a passing unit test in the ProductTest test case for salable items. If the index( ) action simply uses the Product to find salable items, aren’t we covered? Well, our model is covered, but now we need to test that the controller action handles a web request, creates the proper objects for the view, and then renders the view. That is, we’re testing at a higher level than the model. Could we have simply tested the controller and, because it uses the model, not written unit tests for the model? Yes, but by testing at both levels we can diagnose problems quicker. If the controller test fails, but the model test doesn’t, then we know there’s a problem with the controller. If, on the other hand, both tests fail, then our time is best spent focusing on the model. But enough preaching. Adding to the Cart Our next task is to test the add_to_cart( ) action. Sending a product id in the request should put a cart containing a corresponding item in the session and then redirect to the display_cart()action. File 119 def test_add_to_cart get :add_to_cart, :id => @version_control_book.id cart = session[:cart] assert_equal @version_control_book.price, cart.total_price assert_redirected_to :action => 'display_cart' follow_redirect assert_equal 1, assigns(:items).size assert_template "store/display_cart" end The only tricky thing here is having to call the method follow_redirect()after asserting that the redirect occurred. Calling follow_redirect() simulates the browser being redirected to a new page. Doing this makes the assigns vari- Report erratum TESTING CONTROLLERS 158 able and assert_template( ) assertion use the results of the display_cart() action, rather than the original add_to_cart( ) action. In this case, the display_cart( ) action should render the display_cart.rhtml view, which has access to the @items instance variable. The use of the symbol parameter in assigns(:items) is also worth discussing. For historical reasons, you cannot index assigns with a symbol—you must use a string. Because all the cool dudes use symbols, we instead use the method form of assigns, which supports both symbols and strings. We could continue to walk through the whole checkout process by adding successive assertions in test_add_to_cart(), using follow_redirect( ) to keep the ball in the air. But it’s better to keep the tests focused on a single request/response pair because fine-grained tests are easier to debug (and read!). Oh, while we’re adding stuff to the cart, we’re reminded of the time when the customer, while poking and prodding our work, maliciously tried to add an invalid product by typing a request URL into the browser. The application coughed up a nasty-looking page, and the customer got ner- vous about security. We fixed it, of course, to redirect to the index() action and display a flash notice. The following test will help the customer (and us) sleep better at night. File 119 def test_add_to_cart_invalid_product get :add_to_cart, :id => '-1' assert_redirected_to :action => 'index' assert_equal "Invalid product", flash[:notice] end Checkout! Let’s not forget checkout. We need to end up with an @order instance variable for the checkout.rhtml view to use. File 119 def test_checkout test_add_to_cart get :checkout assert_response :success assert_not_nil assigns(:order) assert_template "store/checkout" end Notice that this test calls another test. The rub is that if the cart is empty, wewon’tgettothecheckoutpageasexpected. So we need at least one item in the cart, similar to what test_add_to_cart( ) did. Rather than duplicating code, we just call test_add_to_cart( ) to put something in the cart. Report erratum TESTING CONTROLLERS 159 Save the Order Last, but certainly not least, we need to test saving an order through the save_order( ) action. Here’s how it’s supposed to work: the cart dumps its items into the Order model, the Order gets saved in the database, and the cart is emptied. Then we’re redirected back to the main store page where a kind message awaits. We’ve mostly been testing the happy path so far, so let’s switch it up by try- ing to save an invalid order, just so we don’t forget about writing boundary condition tests. File 119 def test_save_invalid_order test_add_to_cart post :save_order, :order => {:name => 'fred', :email => nil} assert_response :success assert_template "store/checkout" assert_tag :tag => "div", :attributes => { :class => "fieldWithErrors" } assert_equal 1, session[:cart].items.size end We need items in the cart, so this test starts by calling test_add_to_cart() (sounds like we need another custom assertion). Then an invalid order is sent through the request parameters. When an invalid order is sub- mitted through the checkout.rhtml view, we’re supposed to see red boxes around the fields of the order form that are required but missing. That’s easy enough to test. We cast a wide net by using assert_tag( ) to check the response for a div tag with fieldWithErrors as its class attribute. Sounds like a good opportunity to write another set of custom assertions. File 122 def assert_errors assert_tag error_message_field end def assert_no_errors assert_no_tag error_message_field end def error_message_field {:tag => "div", :attributes => { :class => "fieldWithErrors" }} end As we are writing these tests, we run the tests after every change to make sure we’re still working on solid ground. The test results now look as follows. depot> ruby test/functional/store_controller_test.rb Loaded suite test/functional/store_controller_test Started Finished in 1.048497 seconds. 5 tests, 28 assertions, 0 failures, 0 errors Report erratum TESTING CONTROLLERS 160 Excellent! Now that we know an invalid order paints fields on the page red, let’s add another test to make sure a valid order goes through cleanly. File 119 def test_save_valid_order test_add_to_cart assert_equal 1, session[:cart].items.size assert_equal 1, Order.count post :save_order, :order => @valid_order_for_fred.attributes assert_redirected_to :action => 'index' assert_equal "Thank you for your order.", flash[:notice] follow_redirect assert_template "store/index" assert_equal 0, session[:cart].items.size assert_equal 2, Order.find_all.size end Rather than creating a valid order by hand, we use the @valid_order_for_fred instance variable loaded from the orders fixture. To put it in the web request, call its attributes( ) method. Here’s the orders.yml fixture file. File 113 valid_order_for_fred: id: 1 name: Fred email: fred@flintstones.com address: 123 Rockpile Circle pay_type: check We’re becoming pros at this testing stuff, so it’s no surprise that the test passes. Indeed, we get redirected to the index page, the cart is empty, and two orders are in the database—one loaded by the fixture, the other saved by the save_order() action. OK, so the test passes, but what really happened when we ran the test? The log/test.log file gives us a backstage pass to all the action. In that file we find, among other things, all the parameters to the save_order() action and the SQL that was generated to save the order. Processing StoreController#save_order (for at Mon May 02 12:21:11 MDT 2005) Parameters: {"order"=>{"name"=>"Fred", "id"=>1, "pay_type"=>"check", "shipped_at"=>Mon May 02 12:21:11 MDT 2005, "address"=>"123 Rockpile Circle", "email"=>"fred@flintstones.com"}, "action"=>"save_order", "controller"=>"store"} Order Columns (0.000708) SHOW FIELDS FROM orders SQL (0.000298) BEGIN SQL (0.000219) COMMIT SQL (0.000214) BEGIN SQL (0.000566) INSERT INTO orders (‘name‘, ‘pay_type‘, ‘shipped_at‘, ‘address‘, ‘email‘) VALUES( 'Fred', 'check', '2005-05-02 12:21:11', '123 Rockpile Circle', 'fred@flintstones.com') SQL (0.000567) INSERT INTO line_items (‘order_id‘, ‘product_id‘, ‘quantity‘, ‘unit_price‘) VALUES(6, 1, 1, 29.95) SQL (0.000261) COMMIT Redirected to http://test.host/store Completed in 0.04126 (24 reqs/sec) | Rendering: 0.00922 (22%) | DB: 0.00340 (8%) Report erratum 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 compre- hensive 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 proces- sor 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, Payment- Gateway object. Using a mock frees the tests from needing a network connection and ensures more consistent results. Thankfully, Rails makes mocking objects a breeze. 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,justthemethodswewanttoredefine 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 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, theflashnoticeshouldberenderedinthe 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 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 Report erratum RUNNING TESTS WITH RAKE 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. 12.6 Running Tests with Rake Rake 4 is a Ruby program that builds other Ruby programs. It knows how to build those programs by reading a file called Rakefile, which includes a set of tasks. Each task has a name, a list of other tasks it depends on, and a list of actions to be performed by the task. When you run the rails script to generate a Rails project, you automatically get a Rakefile in the top-level directory of the project. And right out of the chute, the Rakefile you get with Rails includes handy tasks to automate recurring project chores. To see all the built-in tasks you can invoke and their descriptions, run the following command in the top-level directory of your Rails project. depot> rake tasks Let’s look at a few of those tasks. Make a Test Database One of the Rake tasks we’ve already seen, clone_structure_to_test, clones the structure (but not the data) from the development database into the test database. To invoke the task, run the following command in the top-level directory of your Rails project. depot> rake clone_structure_to_test 4 http://rake.rubyforge.net Report erratum [...]... "Product.search('version_control')" 10 % cumulative self self total time seconds seconds calls ms/call ms/call name 68.61 46 .44 46 .44 10 46 44. 00 6769.00 Product#search 8.55 52.23 5.79 100000 0.06 0.06 Fixnum#+ 8.15 57.75 5.52 100000 0.06 0.06 Math.sqrt 7 .42 62.77 5.02 100000 0.05 0.05 IO#gets 0. 04 68.95 0.03 10 3.00 50.00 Product#find OK, the top contributors to the search( ) method are some math and I/O we’re... 12096000 241 92000 51 840 000 630720000 You can also calculate times relative to Time.now using the methods ago( ) and from_now( ) (or their aliases until( ) and since( ), respectively) puts puts puts puts puts Time.now 20.minutes.ago 20.hours.from_now 20.weeks.from_now 20.months.ago #=> #=> #=> #=> #=> Tue Tue Wed Tue Thu May May May Sep Sep 10 10 11 27 18 17:03 :43 16 :43 :43 13:03 :43 17:03 :43 17:03 :43 CDT... interact with running Rails applications We talk about this starting on page 187 console Allows you to use irb to interact with your Rails application methods irb → page 47 8 destroy Removes autogenerated files created by generate generate A code generator Out of the box, it will create controllers, mailers, models, scaffolds, and web services You can also download additional generator modules from the Rails. .. to point to the right database development: development_ sqlite development_ mysql: adapter: mysql database: depot _development host: localhost username: password: development_ sqlite: adapter: sqlite dbfile: my_db If changing to a different database also changes other things in your application’s configuration, you can create multiple sets of environments (development- mysql, development- postgres, and so... | Helpers | 15 | 11 | 0 | 1 | 0 | 9 | | Controllers | 342 | 2 14 | 5 | 27 | 5 | 5 | | APIs | 0 | 0 | 0 | 0 | 0 | 0 | | Components | 0 | 0 | 0 | 0 | 0 | 0 | | Functionals | 228 | 142 | 7 | 22 | 3 | 4 | | Models | 208 | 108 | 6 | 16 | 2 | 4 | | Units | 193 | 128 | 6 | 20 | 3 | 4 | + + -+ -+ -+ -+ -+ -+ | Total | 986 | 603 | 24 | 86 | 3 | 5 | + + -+ -+ -+ -+ -+... irb(main):003:0> pr.price = 34. 95 => 34. 95 irb(main):0 04: 0> pr.save => true Logging and tracing are a great way of understanding the dynamics of complex applications You’ll find a wealth of information in the development log file When something unexpected happens, this should probably be the first place you look It’s also worth inspecting the web server log for anomalies If you use WEBrick in development, this will...R UNNING T ESTS WITH R AKE Running Tests You can run all of your unit tests with a single command using the Rakefile that comes with a Rails project depot> rake test_units Here’s sample output for running test_units on the Depot application depot_testing> rake test_units (in /Users/mike/work/depot_testing) Started Finished in 0.8739 74 seconds 16 tests, 47 assertions, 0 failures, 0... we didn’t test everything However, with what we now know, we could test everything Indeed, Rails has excellent support to help you write good tests Test early and often—you’ll catch bugs before they have a chance to run and hide, your designs will improve, and your Rails application with thank you for it Report erratum 171 Part III The Rails Framework Chapter 13 Rails in Depth Having survived our Depot... on your web site daily After all, you can’t improve that which you don’t measure 12.7 Performance Testing Speaking of the value of measuring over guessing, we might be interested in continually checking that our Rails application meets performance requirements Rails being a web- based framework, any of the various HTTP-based web testing tools will work But just for fun, let’s see what we can do with the... style of web- based application makes use of JavaScript and XMLHttpRequest to provide a far more interactive user experience Chapter 18, The Web, V2.0, tells you how to spice up your applications Report erratum 188 W HAT ’ S N EXT Rails can do more than talk to browsers Chapter 19, Action Mailer, shows you how to send and receive e-mail from a Rails application, and Chapter 20, Web Services on Rails, . name 68.61 46 .44 46 .44 10 46 44. 00 6769.00 Product#search 8.55 52.23 5.79 100000 0.06 0.06 Fixnum#+ 8.15 57.75 5.52 100000 0.06 0.06 Math.sqrt 7 .42 62.77 5.02 100000 0.05 0.05 IO#gets 0. 04 68.95. Controllers | 342 | 2 14 | 5 | 27 | 5 | 5 | | APIs | 0 | 0 | 0 | 0 | 0 | 0 | | Components | 0 | 0 | 0 | 0 | 0 | 0 | | Functionals | 228 | 142 | 7 | 22 | 3 | 4 | | Models | 208 | 108 | 6 | 16 | 2 | 4 | |. you run the rails script to generate a Rails project, you automatically get a Rakefile in the top-level directory of the project. And right out of the chute, the Rakefile you get with Rails includes