- •Contents
- •Preface to the Second Edition
- •Introduction
- •Rails Is Agile
- •Finding Your Way Around
- •Acknowledgments
- •Getting Started
- •The Architecture of Rails Applications
- •Models, Views, and Controllers
- •Active Record: Rails Model Support
- •Action Pack: The View and Controller
- •Installing Rails
- •Your Shopping List
- •Installing on Windows
- •Installing on Mac OS X
- •Installing on Linux
- •Development Environments
- •Rails and Databases
- •Rails and ISPs
- •Creating a New Application
- •Hello, Rails!
- •Linking Pages Together
- •What We Just Did
- •Building an Application
- •The Depot Application
- •Incremental Development
- •What Depot Does
- •Task A: Product Maintenance
- •Iteration A1: Get Something Running
- •Iteration A2: Add a Missing Column
- •Iteration A3: Validate!
- •Iteration A4: Prettier Listings
- •Task B: Catalog Display
- •Iteration B1: Create the Catalog Listing
- •Iteration B4: Linking to the Cart
- •Task C: Cart Creation
- •Sessions
- •Iteration C1: Creating a Cart
- •Iteration C2: A Smarter Cart
- •Iteration C3: Handling Errors
- •Iteration C4: Finishing the Cart
- •Task D: Add a Dash of AJAX
- •Iteration D1: Moving the Cart
- •Iteration D3: Highlighting Changes
- •Iteration D4: Hide an Empty Cart
- •Iteration D5: Degrading If Javascript Is Disabled
- •What We Just Did
- •Task E: Check Out!
- •Iteration E1: Capturing an Order
- •Task F: Administration
- •Iteration F1: Adding Users
- •Iteration F2: Logging In
- •Iteration F3: Limiting Access
- •Iteration F4: A Sidebar, More Administration
- •Task G: One Last Wafer-Thin Change
- •Generating the XML Feed
- •Finishing Up
- •Task T: Testing
- •Tests Baked Right In
- •Unit Testing of Models
- •Functional Testing of Controllers
- •Integration Testing of Applications
- •Performance Testing
- •Using Mock Objects
- •The Rails Framework
- •Rails in Depth
- •Directory Structure
- •Naming Conventions
- •Logging in Rails
- •Debugging Hints
- •Active Support
- •Generally Available Extensions
- •Enumerations and Arrays
- •String Extensions
- •Extensions to Numbers
- •Time and Date Extensions
- •An Extension to Ruby Symbols
- •with_options
- •Unicode Support
- •Migrations
- •Creating and Running Migrations
- •Anatomy of a Migration
- •Managing Tables
- •Data Migrations
- •Advanced Migrations
- •When Migrations Go Bad
- •Schema Manipulation Outside Migrations
- •Managing Migrations
- •Tables and Classes
- •Columns and Attributes
- •Primary Keys and IDs
- •Connecting to the Database
- •Aggregation and Structured Data
- •Miscellany
- •Creating Foreign Keys
- •Specifying Relationships in Models
- •belongs_to and has_xxx Declarations
- •Joining to Multiple Tables
- •Acts As
- •When Things Get Saved
- •Preloading Child Rows
- •Counters
- •Validation
- •Callbacks
- •Advanced Attributes
- •Transactions
- •Action Controller: Routing and URLs
- •The Basics
- •Routing Requests
- •Action Controller and Rails
- •Action Methods
- •Cookies and Sessions
- •Caching, Part One
- •The Problem with GET Requests
- •Action View
- •Templates
- •Using Helpers
- •How Forms Work
- •Forms That Wrap Model Objects
- •Custom Form Builders
- •Working with Nonmodel Fields
- •Uploading Files to Rails Applications
- •Layouts and Components
- •Caching, Part Two
- •Adding New Templating Systems
- •Prototype
- •Script.aculo.us
- •RJS Templates
- •Conclusion
- •Action Mailer
- •Web Services on Rails
- •Dispatching Modes
- •Using Alternate Dispatching
- •Method Invocation Interception
- •Testing Web Services
- •Protocol Clients
- •Secure and Deploy Your Application
- •Securing Your Rails Application
- •SQL Injection
- •Creating Records Directly from Form Parameters
- •Avoid Session Fixation Attacks
- •File Uploads
- •Use SSL to Transmit Sensitive Information
- •Knowing That It Works
- •Deployment and Production
- •Starting Early
- •How a Production Server Works
- •Repeatable Deployments with Capistrano
- •Setting Up a Deployment Environment
- •Checking Up on a Deployed Application
- •Production Application Chores
- •Moving On to Launch and Beyond
- •Appendices
- •Introduction to Ruby
- •Classes
- •Source Code
- •Resources
- •Index
- •Symbols
USING MOCK OBJECTS 224
Say (as a contrived example) we notice that the User.encrypted_password method seems to be taking far too long. Let’s first find out if that’s the case.
depot> ruby script/performance/benchmarker 'User.encrypted_password("secret", "salt")'
|
user |
system |
total |
|
real |
#1 |
1.650000 |
0.030000 |
1.680000 |
( |
1.761335) |
Wow, 1.8 elapsed seconds to run one method seems high! Let’s run the profiler to dig into this.
depot> ruby script/performance/profiler 'User.encrypted_password("secret", "salt")'
Loading Rails...
Using |
the standard Ruby profiler. |
|
|
|
||
% |
cumulative |
self |
|
self |
total |
|
time |
seconds |
seconds |
calls |
ms/call |
ms/call |
name |
78.65 |
58.63 |
58.63 |
1 |
58630.00 74530.00 |
Integer#times |
|
21.33 |
74.53 |
15.90 |
1000000 |
0.02 |
0.02 |
Math.sin |
1.25 |
75.46 |
0.93 |
1 |
930.00 |
930.00 |
Profiler__.start_profile |
0.01 |
75.47 |
0.01 |
12 |
0.83 |
0.83 |
Symbol#to_sym |
. . . |
|
|
|
|
|
|
0.00 |
75.48 |
0.00 |
1 |
0.00 |
0.00 |
Hash#update |
That’s strange: the method seems to be spending most of its time in the times and sin methods. Let’s look at the source:
def self.encrypted_password(password, salt) 1000000.times { Math.sin(1)} string_to_hash = password + salt Digest::SHA1.hexdigest(string_to_hash)
end
Oops! That loop at the top was added when I wanted to slow things down during some manual testing, and I must have forgotten to remove it before I deployed the application. Guess I lose the use of the red stapler for a week.
Finally, remember the log files. They’re a gold mine of useful timing information.
13.6Using 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 lib/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', |
Report erratum
|
|
USING MOCK OBJECTS |
225 |
: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 back-end 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 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 stubbing out objects a breeze.
To stub 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. Let’s look at the details of naming here.
First, the filename must match the name of the file we’re trying to replace. We can stub out a model, controller, or library file: the only constraint is that the filename must match. Second, look at the path of the stub file. We put it in the test subdirectory of the test/mocks directory. This subdirectory holds all the stub files that are used in the test environment. If we wanted to stub out files while in the development environment, we’d have put our stubs in the directory test/mocks/development.
Now let’s look at the file itself.
require 'lib/payment_gateway'
class PaymentGateway
# I'm a stubbed out method def collect(request)
true end
end
Notice that the stub file actually loads the original PaymentGateway class (using require). It then reopens the PaymentGateway class and overrides just the collect method. That means we don’t have to stub out all the methods of PaymentGateway, just the methods we want to redefine for when the tests run. In this case, the new collect method simply returns a fake response.
With this file in place, the StoreController will use the stub PaymentGateway class. This happens because Rails arranges the search path to include the mock path first—the file test/mocks/test/payment_gateway.rb is loaded instead of lib/payment_gateway.rb.
Report erratum
USING MOCK OBJECTS 226
That’s all there is to it. By using stubs, we can streamline the tests and concentrate on testing what’s most important. And Rails makes it painless.
Stubs vs. Mocks
You may have noticed that the previous section uses the term stub for these fake classes and methods but that Rails places them in a subdirectory of test/mocks. Rails is playing a bit fast and loose with its terminology here. What it calls mocks are really just stubs: faked-out chunks of code that eliminate the need for some resource.
However, if you really want mock objects—objects that test to see how they are used and create errors if used improperly—then Rails has an answer. As of 1.2, Rails includes Flex Mock,6 Jim Weirich’s Ruby library for mock objects. You can use it in any of your tests, but you’ll need to require it explicitly.
require "flexmock"
What We Just Did
We wrote some tests for the Depot application, but 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 will thank you for it.
6. http://onestepback.org/software/flexmock/
Report erratum