6.5. Summary
This chapter had three major sections. First, we
|
Chapter 7. Testing and Debugging
If you are skimming this book, you might be tempted to skip over this chapter. After all, why read about testing until you have an application to test? And why worry about debugging techniques until something goes wrong? The answer is simple: testing and debugging are activities that should be happening at every stage of developmentpossibly even
before
development. Why? Several reasons. It's not surprising that thoroughly
Automated testing is done in every language and platform. But not every technology community places equal weight on the importance of testinga prime example being JavaScript, which is very often written with no tests at all. Fortunately, the Ruby and Rails communities both have a strong culture of testing. A large percentage of Ruby projects include a test suite, and Rails itself has a very thorough one. Rails also encourages developers to test their own applications. Ever notice how script/generate adds little test stubs for every model and controller you generate? That's Rails' way of reminding you to test your code early and often. Ajax adds a new twist to testing and debugging, and it can often be a major source of frustration. In this chapter, we'll explore the tools and techniques that will help make your application stronger and your life easier. Given the wealth of tools that support programming in Ruby and other languages, working in JavaScript can be a frustrating experience. Fortunately, there are a growing number of development-support tools to make your work more productive.
In this chapter, we'll first look at debugging tricks and tools, then testing techniques, and finally a couple of ways to catch any remaining
|
7.1. Debugging
In general, the process of debugging can be boiled down to making the right information visible. All debugging tools attempt to do thatthrough, for example, logfiles, inspectors, and breakpointsthey all help you to break down a complex interaction into smaller
7.1.1. Understanding Rails ExceptionsWhen running in the development environment, any Rails action that results in an error or an exception will result in an exception debugging screen, as in Figure 7-1. Figure 7-1. Typical Rails exception screen
Anyone doing a significant amount of Rails development will become very familiar with messages like this, so it's worthwhile to take a look at what it says. The first few pieces are the most important. The header
In some cases, the problem won't be so apparent. The excerpted code only shows where the exception originated at the view level, but that doesn't
Learning to read the stack trace is an art that takes some experience, but the Rails exception screen makes it more approachable by providing three different views of the data: the application trace, the framework trace, and the full trace. The application trace only shows the
Nine times out of 10, the top line of the application trace will direct you to the source of the bug, but if not,
7.1.2. Using the Development Log
Every programmer has done it: when you are trying to debug a
The
print
statement (or
puts
, as is more common in Ruby) is the blunt instrument of debugging. It doesn't scale. As your development gets more complex, a more refined tool is called for, and that tool is a
logger
. By using an application-wide logger, your debugging messages are rationalized and decoupled from the normal application flow, meaning there's no danger of
The most basicand essentialdebugging tool is a Rails application's logfiles, which record details about every request received by the application. Stored in each project's
log
directory, there is one logfile for each environment: development, production, and test. Each environment can have different log levels, so for example,
production.log
won't
If you are running your Rails application via
script/server
, the development logfile will automatically be printed to the console. Otherwise, the
tail
utility (standard on Unix-like systems, including OS X but also available in Windows from http://tailforwin32.
Processing ArticlesController#edit (for 127.0.0.1 at 2006-07-04
23:22:42) [GET]
Session ID: ab095483400d6b99f4c7b61d4b7dc70c
Parameters: {"action"=>"edit", "id"=>"1", "controller"=>"articles"}
Article Columns (0.001233) SHOW FIELDS FROM articles
Article Load (0.001884) SELECT * FROM articles WHERE
(articles.id = 1) LIMIT 1
Rendering within layouts/articles
Rendering articles/edit
Completed in 0.06104 (16 reqs/sec) Rendering: 0.03784 (61%) DB:
0.00312 (5%) 200 OK [http://localhost/articles/1;edit]
Processing ArticlesController#update (for 127.0.0.1 at 2006-07-04
23:23:03) [PUT]
Session ID: ab095483400d6b99f4c7b61d4b7dc70c
Parameters: {"article"=>{"title"=>"Using logfiles", "body"=>"The
logfiles are invaluable."}, "commit"=>"Save", "_method"=>"put",
"action"=>"update", "id"=>"1", "controller"=>"articles"}
Article Columns (0.001387) SHOW FIELDS FROM articles
Article Load (0.001925) SELECT * FROM articles WHERE
(articles.id = 1) LIMIT 1
SQL (0.000751) BEGIN
Article Update (0.027114) UPDATE articles SET 'title' = 'Using
logfiles', 'body' = 'The logfiles are invaluable.', 'user_id' = 1
WHERE id = 1
SQL (0.001924) COMMIT
Redirected to http://localhost:3000/articles/1
Completed in 0.08334 (11 reqs/sec) DB: 0.03310 (39%) 302 Found [http://localhost/articles/1]
Take a look at all of the information available here: for each request, the first line shows which controller and action have been dispatched, with which HTTP method. Next, the session ID is noteduseful for debugging session and cookie-
Rails uses Ruby's standard logging tool. It's available (as
logger
) to use from within your Rails application from
class Message < ActiveRecord::Base
def after_initialize
logger.info "This is the model"
end
end
To write to the logfile from a controller action, the usage is the same: def show logger.info "This is the controller" @post = Message.find params[:id] end To write to the logfile from the view, just include a call to logger in an RHTML template: <% logger.info "This is the view" %> The resulting entry in development.log would show all three messages:
Processing MessagesController#show [GET]
Parameters: {"action"=>"show", "id"=>"1", "controller"=>"messages"}
This is the controller
Message Columns (0.002579) SHOW FIELDS FROM messages
Message Load (0.001082) SELECT * FROM messages WHERE (id = 1) LIMIT 1
This is the model
Rendering within layouts/application
Rendering messages/show
This is the view
Completed in 0.10790 (9 reqs/sec) 200 OK [http://localhost/messages/1]
To access the logger from the console or your tests, use the constant RAILS_DEFAULT_LOGGER . 7.1.3. Interactive Consoles7.1.3.1. Using IrbPerhaps the most essential tool in the toolbox of the Rails developer is the interactive shell, or console. Using Ruby's standard Interactive Ruby (Irb) library, the console allows you to access every part of your application from the command line. You can load ActiveRecord objects, inspect and edit data, and save the changes back to the database. You can simulate controller requests and inspect the resultnot just the raw HTML response, but the template assignments, session state, flashes, and more. If you are new to Irb, let's give it a quick spin. From your command prompt, run irb and you'll get a prompt like this: irb(main):001:0> Now enter in any Ruby snippet (say, 123 + 456 ), hit Enter, and Irb will print the output: irb(main):001:0> 123 + 456 => 579
Remember that in Ruby, everythingeven integersare objects with
irb(main):002:0> 123.class => Fixnum
Another
irb(main):003:0> 123.methods => ["method", "%", "between?", "send", "<<", "prec", "modulo", "&", "object_id", ">>", "zero?", "size", "singleton_methods", "__send_ _", "equal?", "taint", "id2name", "*", "next", "frozen?", "instance_variable_get", "+", "kind_of?", "step", "to_a", "instance_eval", "-", "remainder", "prec_i", "nonzero?", "/", "type", "protected_methods", "extend", "floor", "to_sym", "", "eql?", "display", "quo", "instance_variable_set", "~", "hash", "is_a?", "downto", "to_s", "prec_f", "abs", "singleton_method_added", "class", "tainted?", "coerce", "private_methods", "^", "ceil", "untaint", "+@", "upto", "-@", "div", "id", "**", "times", "to_i", "<", "inspect", "<=>", "==", ">", "===", "succ", "clone", "public_methods", "round", ">=", "respond_to?", "<=", "freeze", "divmod", "chr", "to_f", "__id_ _", "integer?", "=~", "methods", "nil?", "dup", "to_int", "instance_variables", "[]", "instance_of?", "truncate"]
What's going on in these examples? Regular
irb(main):004:0> 123.next => 124
It turns out to be very simple: it just adds one to the value. Introspection is a hugely
7.1.3.2. Using the Rails console with ActiveRecordNow let's quit Irb (enter quit ) and switch to the Rails console, which is really just a wrapper around Irb, specialized to automatically include your application's entire environment. The console can be used to drive your application on different levelseither directly with domain objects (i.e., ActiveRecord objects, or models), or by simulating requests to controllers. To get a feel for working with models, suppose you have a simple database structure consisting of articles and users, with a one-to-many relationship. The corresponding models have basic associations and validations: class Article < ActiveRecord::Base belongs_to :user validates_presence_of :title end class User < ActiveRecord::Base has_many :articles validates_presence_of :name end With that structure in place, the Rails console provides a rich environment for interacting with the domain model. To try it out, run script/console , and explore a little:
$ script/console
Loading development environment.
>> a = Article.new
=> #<Article:0x22409e8 @attributes={"user"=>nil, "title"=>nil,
"body"=>nil}, @new_record=true>
>> a.name = "Using script/console"
=> "Using script/console"
>> a.save
=> true
>> User.create :name => "Scott"
=> #<User:0x22289c4 @attributes={"name"=>"Scott", "id"=>1}>
>> u=User.find :first
=> #<User:0x223587c @attributes={"name"=>"Scott", "id"=>"1"}>
>> a.update_attributes :user_id => u.id
=> true
>> u.articles
=> [#<Article:0x222bc64 @attributes={"body"=>nil, "title"=>
"Using script/console", "id"=>"1", "user_id"=>"1"}>]
>> u.articles.create
=> #<Article:0x2223758 @attributes={"body"=>nil, "title"=>nil,
"user_id"=>1}, @errors=#<ActiveRecord::Errors:0x2222efc, @errors={
"title"=>["can't be blank"]}>>
Take a look at all that's happening here. First, we instantiate a new, unsaved ActiveRecord object. That record is assigned to the variable a , and a representation of it is printed to the terminal ( #<...> is Ruby's standard way of representing objects textually). Because ActiveRecord automatically creates getter and setter methods for every database column, you can easily change the object's attributes ( a.title='...' ), and then save the record to the database ( a.save ). The next line illustrates the same process, condensed to just one line, using the create method. Then, using update_attributes , we create an association between the two records, and access the user's articles association, which returns an array of Article objects. Finally, we attempt to create a new article, but aren't successful, because of a validation error, which is visible from the console output. As you can see, the full ActiveRecord API is available from within the Rails console, making it an invaluable debugging tool for model-related issues. 7.1.3.3. Using the Rails console with ActionPackThe console isn't limited to interacting with your models; it can interact with your controllers as well. The key is the app object, which is an instance of ActionController 's Integration Session class. An Integration Session acts like a virtual user of your web applicationone with state (like cookies), so that you can perform a series of requests.
The
app
object that's available in the console has a method for each major HTTP verb (
get
,
post
,
put
, and
delete
). The first argument is the URL
Take a look at this example of using an integration session: script/console Loading development environment. >> app.get '' => 200 >> app.response.body[0..150] => "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lan "
The first line sends an HTTP
GET
request to the root URLpresumably the site's home page. The result of the call is the HTTP response status code 200, indicating a successful response. The next line requests the first 150
>> app.post "/signin", :person => { :login => 'scott', :password => 'secret' }
=> 302
Here we send an HTTP POST to the /signin pathwhich in this case, is hooked up to an action that authenticates a user and stores that user in the session. The result this time is the HTTP status code 302, indicating a redirect. Just what we'd expect for a successful login. Don't forget that you can mix ActiveRecord calls with Integration Session calls:
>> Person.count
=> 1
>> app.post "/people", :person => { :name => "David Jones", :email => "dj@example.com" }
=> 302
>> Person.count
=> 2
In this example we're doing a little bit of
So far we've been hand-writing the paths for the requests (e.g., "/people" in the example above), but we could also use the familiar url_for helper and named routes to generate URLs. For example: >> app.person_url :id => 1 => "http://www.example.com/people/1" >> app.delete app.person_url(:id => Person.find(:first)) => 302 >> Person.count => 1 In this case, we see an example of accessing named routes from the console, in order to verify that they are behaving as expected. And finally, an example of the HTTP DELETE method and another check to make sure it truly worked. Integration Sessions and the app object were added to Rails to support integration testing, which is discussed in detail later in this chapter. 7.1.3.4. Rails breakpoints
Breakpoints are an invaluable debugging aid, allowing you to
Suppose we're developing an application with a message resource and working on the template for the show action. Reloading the page in the browser, we get a Rails exception message: NoMethodError in Messages#show undefined method 'title' for #<Message:0x4fd16c8> Not sure why the action is failing, we decide to use breakpoints to explore the problem. Switching to the controller, we add the breakpoint command to the action: def show @post = Message.find params[:id] breakpoint end Save it, switch back to the browser, and refresh. The browser will start to reload the page but won't ever finish. When Rails encounters the breakpoint statement, it suspends execution and allows you to enter a console session in the midst of the action. To enter the breakpoint, use the script/breakpointer command, just like the script/console command. The result looks like this: $ script/breakpointer Executing break point at script/../config/../app/controllers/messages_controller.rb:20 in 'show' irb(#<MessagesController:0x4f2d0f0>):001:0> Notice that the prompt informs you which breakpoint you are in. The instance_variables method returns an array of instance variables available in the current context: irb(#<MessagesController:0x2485730>):004:0> instance_variables => ["@post", "@headers", "@performed_redirect", "@flash", "@request", "@_ _bp_file", "@assigns", "@action_name", "@params", "@current_user", "@session", "@template", "@cookies", "@_ _bp_line", "@request_origin", "@performed_render", "@variables_added", "@response", "@url", "@before_filter_chain_aborted"] Of course, you can also access any methods that are available in the controller, such as params :
irb(#<MessagesController:0x4f2d0f0>):001:0> params
=> {"action"=>"show", "id"=>"7", "controller"=>"messages"}
Because the breakpoint was called after the @post instance variable was set by the action, you can inspect its contents:
irb(#<MessagesController:0x4f2d0f0>):002:0> @post
=> #<Message:0x4f0bcc0 @attributes={"name"=>"Untitled Message",
"body"=>"", "id"=>"7", "created_at"=>"2006-07-06 21:02:48"}>
And, narrowing in on the problem at hand, you might ask for a list of just the model's attributes: irb(#<MessagesController:0x4f2d0f0>):003:0> @post.attributes.keys => ["name", "body", "id", "created_at"] Breakpoints can be added to views as well: just add <% breakpoint %> somewhere in a template file. When you're finished examiningor even modifyingthe state of the application, enter exit to close the breakpointer session. The action will pick up where it left off and send a response to the browser. 7.1.4. Inspectors
The essential goal of debugging is visibility: seeing the hidden properties of objects in the system. So far we've
7.1.4.1. Using the debug helper
The
debug
helper available in your Rails templates is incredibly simple: pass it any object, and it will convert the object to YAML (Yet Another Markup Language) format, wrapped in
<pre>
tags. In other words, it's a
Figure 7-2. Using the debug helper
The debug helper can be particularly powerful when working on a Rails application with a team of people. For example, suppose your team includes a frontend developer who is responsible for the view layer: markup, JavaScript, CSS, etc. While he's a pro at the client-side technologies, Rails isn't his forte, and tools like the console might be intimidating. On the other hand, the back-end developer is most comfortable (and productive) working with databases, ActiveRecord , associations, etc.but not-so-skilled in UI design and browser idiosyncrasies. The debug helper provides a great bridge. Imagine a two-stage development process. First, the back-end developers can build the database structure, create the models, and sketch in rough controllers to define instance variables for each action. For the view, they simply create one debug statement for every instance variable assigned to the view.
Then it's tossed over the wall to the frontend developer, who can clearly see what information is available in the view, without having to reference the database schema or the other
7.1.4.2. RJS debugging modeRJS (explained in Chapter 5) is the secret ingredient that makes Ajax in Rails so powerful and easy. But there is an Achilles' heel: RJS can be frustrating to debug. The reason is simple. When you navigate to a normal HTML page and something goes wrong on the server, Rails returns an exception screen full of details about what went wrong and how to find it. But with RJS, the expected result isn't HTMLit's JavaScript for the browser to evaluate. If there's a bug in the generated JavaScript, the browser will simply fail to execute it, and you won't have access to the source.
The solution is RJS debugging mode. It's on by default in the development environment; to manually
config.action_view.debug_rjs = true When on, any JavaScript generated by RJS templates will be automatically wrapped in a TRy/catch blockso if an exception occurs while it's being evaluated, you'll be notified with two alert boxes. The first simply provides the exception message. The second is the most important: the full source of the JavaScript being evaluated. Often, that information is more than sufficient to spot and eliminate the bug. Keep in mind, however: RJS debugging mode works when there is an error in the JavaScript executionnot when there is a Rails exception in the action or the RJS code. For those problems, the development log is the best place to turn. 7.1.4.3. Routing Navigator
As a Rails application
Routing Navigator eases the pain. It's a plug-in that adds a powerful set of utilities to your applicationright in the browserto help explore your routes. Best of all, it is automatically disabled in the production environment, so you can safely leave it installed, confident that your users will never see it by mistake. To install it, run this command from the root directory of your Rails project: script/plugin install \ http://svn.techno-weenie.net/projects/plugins/routing_navigator
Then add these two lines to your application's layout file(s), to include the necessary JavaScript and CSS files (which should have been automatically
<%= javascript_include_tag 'routing_navigator' %> <%= stylesheet_link_tag 'routing_navigator' %> Once done, you'll see a row of links added to the bottom of every page of your application in development mode, as seen in Figure 7-3. Each link provides a different view of your routes. For example, "Named Routes" details the named routes defined for the current controller, along with their requirements and conditions, as seen in Figure 7-4. Figure 7-3. Using the Routing Navigator plug-in
Figure 7-4. The Routing Navigator expanded
7.1.4.4.
|