Section 7.2. Testing


7.2. Testing

Automated testing is a development practice that Railsas opinioned softwarebelieves in strongly. Rails divides tests into three groups: unit tests, which cover your models; functional tests, which cover your controllers; and integration tests, which also exercise controllers, but at a higher level. Since ActiveRecord is outside of the scope of this book, we won't look at unit tests, instead focusing on functional and integration tests.

7.2.1. Functional Tests

The goal of functional testing is to isolate each action in your controllers and verify that they behave as expected. As the simplest level, that means providing some amount of input (in the form of fixtures, sessions, query parameters, or request body) and then verifying the result (e.g., response body, headers, session, database changes).

To accomplish that, Rails uses Ruby's standard testing framework, Test::Unit. Let's look at an example. Suppose you have a simple, one-action controller with a before filter, like this:

class PeopleController < ApplicationController      before_filter :require_login   def index     @people = Person.find :all   end end

To make sure that it works, at least roughly, we'd create a test like this:

class PeopleControllerTest < Test::Unit::TestCase   fixtures :people   def setup     @controller = PeopleController.new     @request    = ActionController::TestRequest.new     @response   = ActionController::TestResponse.new     login   end   def test_index     get :index     assert_response :success     assert_template 'index'     assert_not_nil assigns(:people)   end end

The first line of the test class is fixtures :people, which takes care of loading test data into the people table of the test database so that the tests have something to work with. Fixture data is stored in the test/fixtures directory.

The setup method is called before every test, effectively wiping the slate clean so that your tests won't have any effect on each other. Notice that I added a login call to setup, to take care of simulating a user signing in. I defined that helper method in test/test_helper.rb, like so:

def login person=:scott   @request.session[:person_id] = people(person).id end

To run the test, enter rake test:functionals from the project's root directory. The output will look like this:

Loaded suite people_controller_test Started . Finished in 0.930592 seconds. 1 tests, 3 assertions, 0 failures, 0 errors

"0 failures, 0 errors" is the sound of success in Rails testing. Although distinguishing between errors and failures may sound redundant, there are actually two ways a test can go wrong. First, the test framework catches any exception that's thrown while processing an action. Test::Unit calls these errors. A failure is different: a failure represents any time an assertion isn't true.

Because functional tests fail when an exception is raised, it's worthwhile to create functional tests for your actions even if you don't create any assertions. Of course, it's a good idea to include more specific assertions as well, but simply testing that the action runs without errors will catch a large class of bugs, so it's certainly better than nothing.

As we flesh out PeopleController, we'd likely add the rest of the standard CRUD Rails actions (index, new, create, show, edit, update, and destroy). A typical set of corresponding functional tests might look like this:

def test_show   get :show, :id => people(:scott).id   assert_response :success   assert_template 'show'   assert_not_nil assigns(:person)   assert assigns(:person).valid? end def test_new   get :new   assert_response :success   assert_template 'new'   assert_not_nil assigns(:person) end def test_create   num = Person.count   post :create, :person => { :name     => "Scott Raymond",                              :email    => "scott@example.com",                              :password => "secret" }   assert_response :redirect   assert_redirected_to :action => 'edit'   assert_equal num + 1, Person.count end def test_edit   get :edit, :id => people(:scott).id   assert_response :success   assert_template 'edit'   assert_not_nil assigns(:person)   assert assigns(:person).valid? end def test_update   post :update, :id => people(:scott).id   assert_response :redirect   assert_redirected_to :action => 'edit' end def test_destroy   assert_not_nil Person.find(people(:scott).id)   post :destroy, :id => people(:scott).id   assert_response :redirect   assert_redirected_to :action => 'index'   assert_raise(ActiveRecord::RecordNotFound) {     Person.find(people(:scott).id)   } end

These tests are all a bit optimistic: they all start with normal, valid input and assert that things go right from there. That's a good first start, but more thorough tests will go further. You might request a page that doesn't exist, and assert that a 404 is returned. Or you might POST data that's invalid, and assert that an error message is returned.

Rails provides a number of assertion methods that aren't covered here, including the ability to make assertions testing for the presence of certain DOM elements and content. See the Rails API docs for a list of the available assertions.

7.2.2. Testing RJS

Once you start making assertions about the HTML returned by your actions, you won't want to leave your RJS out in the cold, either. Rails doesn't have RJS-specific assertions built in, but there is a plug-in to help fill the need: Another RJS Testing System ( ARTS). To install it, use script/plugin from project directory in the command prompt:

script/plugin install http://thar.be/svn/projects/plugins/arts

With that, you'll suddenly have a slew of new assertions available from within your functional tests. For example:

assert_rjs :alert,   'Hello from RJS' assert_rjs :show,    :my_div, :my_div_2 assert_rjs :hide,    :my_div assert_rjs :remove,  :my_div assert_rjs :toggle,  :my_div assert_rjs :replace, :my_div assert_rjs :replace, :my_div, '<p>This replaced the div</p>' assert_rjs :replace, :my_div, /replaced the div/ assert_rjs :replace_html,  :my_div, "This goes inside the div" assert_rjs :insert_html,   :bottom, :my_div assert_rjs :visual_effect, :highlight, :my_div, :duration => '1.0'

As of this writing, ARTS has a major limitation: it can't be used to test RJS statements that use JavaScript proxies, including element proxies, collection proxies, and class proxies. For example, this RJS statement uses an element proxy, so there is no way to test it with an ARTS assertion:

page[:my_div].show

In order to be testable with assert_rjs, the RJS would need to be rewritten without an element proxy, like this:

page.show :my_div

Support for JavaScript proxies is planned for a future release of the ARTS plug-in, so keep an eye on the developer's weblog (http://glu.ttono.us) for announcements (not to mention a wealth of other information or Rails development and testing).

In the meantime, certain RJS proxy constructions can be tested with one of Rails' built-in assertions, assert_select_rjs. For example:

# Assert an RJS element proxy is created for #foo assert_select_rjs "foo" # Assert the #foo element is updated via an element proxy assert_select_rjs :update,  "foo" # Assert that an insertion is created for the #foo element assert_select_rjs,   :insert, :top, "foo"

7.2.3. Testing HTML Validity

Many of the problems that arise in client-side web development can be avoided with one simple tool: markup validation. Browsers are notoriously lax in parsing HTML, and will usually make a best attempt to display even the most ill-formed of markup. Unfortunately, that creates a downward spiral, where developers are careless about the markup they produce. Because there aren't standardized failure modes across browsers, each one might interpret broken markup differentlyleaving the developer with quite a mess. Once Ajax and DOM scripting is involved, the mess becomes even stickier. For example, the HTML spec says that the ID attribute must be unique for every element in a document. For an app of any complexity, breaking that rule is an easy mistake to makebut it can be a pain to debug. If your JavaScript tries to update the element with that ID, one browser may work as expected, while another fails spectacularly.

The best way to avoid the mess is with markup validation, which acts a little like a compiler for your HTML: it alerts you to tiny mistakes and oversights, so that you are assured to be working on a firm foundation.

The most common and authoritative markup validator is maintained by the W3C at http://validator.w3.org. You can provide a URL or XHTML/HTML source, and it will return any validation problems with the source.

While that's a great tool for one-off validation, it quickly becomes tedious to use repeatedly. Because it's so tedious, it's almost certain that you won't use it when you need it most: during the phases of fast development and rapid iteration before shipping code. Markup validation should be fully integrated with your automated test suite so that it can be run several times a day. That way, once the foundation of valid HTML is in place, you can be confident that it will never develop any cracksor at least you'll be notified right away.

The easiest way to accomplish automated markup validation is with the assert_valid_markup Rails plug-in. As the name suggests, it provides a simple new assertion for the regular Rails functional tests. To install the plug-in, change to your Rails project directory and run:

script/plugin install \   http://redgreenblu.com/svn/projects/assert_valid_markup

The assert_valid_markup plug-in automates the process of interacting with the W3C validator. It's able to simulate a request to one of your Rails actions, send the response HTML to the W3C validator service, and integrate the results back into your functional tests. To try it, just use the regular get method to request an action, then call the assert_valid_markup method to validate the markup contained in @response.body. For example, suppose you have a functional test for an action called :index.

def test_index   get :index   assert_response :success   assert_template 'index'   assert_valid_markup end

This test first simulates an HTTP GET request and stores the response in @response. The first assertion checks that the response status code is in the 200 range, indicating success. The second assertion checks that the expected view template was used to construct the response. And the last assertion passes the HTML through the validator, and reports back any errors found. Because it can be time-consuming to use an external web service repeatedly with every test run, assert_valid_markup caches the results so that the validator is only hit when the response body changes.

It's also possible to use assert_valid_markup as a class method, as opposed to an instance method. In that form, you can give it a list of actions, and it will create a markup test for each.

class ArticlesControllerTest < Test::Unit::TestCase   assert_valid_markup :index, :new end

Every time you create a new action, consider defining a quick markup-validation test right away. With them in place from the beginning, you'll be free to quickly iterate your markup code with confidence, knowing that the foundation will remain firm.

7.2.4. Integration Tests

Integration tests and functional tests cover much of the same ground. They both focus on calling controllers and making assertions about the responses. So why have both kinds of test?

The difference is that functional tests are designed to be narrow: to test one action of one controller at a time. That narrowness is a good thing, because it means each test will be focused on a small piece of functionality, and if the test fails, you'll be able to quickly identify and fix the bug.

But even a full complement of functional tests leaves something to be desired. Sometimes, you'd like to confirm that a sequence of interactions behaves as expectedinteractions that span across multiple controllers, or even multiple users. Integration tests provide just that. They work at a higher level than functional tests and do a better job simulating real users. Here is an example, demonstrating that one integration test typically covers multiple controllers, formalizing a story of how a user interacts with the site.

class CartTest < ActionController::IntegrationTest   fixtures :people, :downloads      def test_add_to_cart     post "/sessions", :person => { :email => people(:scott).email,                       :password => people(:scott).password }     assert_response :redirect          post "/cart_items", :id => downloads(:manhattan).id     assert_response :success     assert_equal 'text/javascript; charset=UTF-8',                  response.headers['type']          get "/cart_items"     assert_response :success     assert_template "cart_items/index"   end end

As your integration tests grow, it's useful to break the stories into smaller chunks, so that they can be composed together into larger tests. That's accomplished by using helper methods in the integration test. For example:

class CartTest < ActionController::IntegrationTest   fixtures :people, :downloads        def test_signin     go_home     signin     assert_response :success     assert_template 'people/show'   end      def test_orders     get orders_url # signin required     assert_redirected_to new_session_url     signin     assert_template "orders/index"   end      private        def go_home       get home_url       assert_response :success       assert_template 'about/home'     end        def signin person=:scott       get new_session_url       assert_response :success       assert_template 'sessions/new'       post sessions_url, :person => { :email => people(person).email,                                  :password => people(person).password }       assert_response :redirect       follow_redirect!     end end

At this stage, the test methods ( test_signin and test_orders) are nice and short, allowing us to see clearly what they're testing. By pulling out some of the common patterns into private methods (like signin), we're keeping the tests DRY. But it's possible to go even further, and actually create a domain-specific language for testing your application. Integration tests provide a method called open_session that returns a new instance of the Integration Session class discussed earlier in this chapter. By adding new methods to that object using extend, your tests can become even more readable. For example:

class CartTest < ActionController::IntegrationTest   fixtures :people      def test_signin     scott = open_session     scott.extend TestExtensions     scott.goes_home     scott.signs_in   end      private        module TestExtensions       def goes_home         get home_url         assert_response :success         assert_template 'about/home'       end       def signs_in person=:scott         get new_session_url         assert_response :success         assert_template 'sessions/new'         post sessions_url, :person => {                      :email => people(person).email,                      :password => people(person).password }         assert_response :redirect         follow_redirect!       end     end end

The open_session method also takes a block, allowing you to encapsulate individual sessions:

class CartTest < ActionController::IntegrationTest   fixtures :people, :downloads, :categories      def test_new_customer_purchase     new_session do |mary|       mary.goes_home       mary.goes_to_signup       mary.signs_up_with :name => "Mary Smith",          :email => "mary@example.com", :password => "secret"       mary.goes_to_category :icons       mary.looks_at_product :manhattan       mary.adds_to_cart :manhattan       mary.goes_to_cart     end   end      private     def new_session person=nil       open_session do |sess|         sess.extend TestExtensions         sess.signs_in(person) unless person.nil?         yield sess if block_given?       end     end        module TestExtensions       def goes_home         get home_url         assert_response :success         assert_template 'about/home'       end              def goes_to_signup         get new_person_url         assert_response :success         assert_template 'people/signup'       end              def signs_up_with options         post people_url, :person => options         assert_response :redirect       end              def goes_to_category category         get category_url(:id => categories(category).slug)         assert_response :success         assert_template "categories/show"       end       def looks_at_product product         get product_url(:id => downloads(product).slug)         assert_response :success         assert_template "products/show"       end              def adds_to_cart product         post cart_items_url, :id => downloads(product).id         assert_response :success       end              def goes_to_cart         get cart_items_url         assert_response :success       end     end      end

By gradually building up a library of integration test extensions, you are creating a testing vocabulary that can be recomposed into new test cases. And by virtue of being so readable and story-like, you can involve less technical members of the team in the process.

Many agile development methodologies emphasize the importance of creating user stories: short scenarios describing how the application will be used from the perspective of the end user. Integration tests are a natural fit for this style of development. You might even start your project by writing natural, English-like stories in your integration tests and then write the code that makes the stories come true.

7.2.5. JavaScript Unit Testing

Complex Ajax applications often involve building an application-specific JavaScript library in application.js or other application-specific files. Once it grows beyond trivial functionality, JavaScript unit testing may be called for, to help verify that your JavaScript behaves as expected.

JavaScript unit testing is conceptually the same as Rails unit testing: the idea is to isolate a small piece of code (usually a single method), give it a controlled input, run it, and use assertions to make sure that it did what it was supposed to do. Unlike Rails unit tests and functional tests, JavaScript unit tests run inside the browser.

The script.aculo.us distribution includes a JavaScript unit-testing framework in unittest.js. It's not included in the standard Rails application skeleton, but it's easy to incorporate, thanks to the JavaScript Test plug-in. To install it, run script/plugin at the console from within your project directory, like this:

script/plugin install \ http://dev.rubyonrails.org/svn/rails/plugins/javascript_test

The plug-in installs a new generator for creating JavaScript test stubs, which will generally correspond to each application-specific JavaScript file in your application. So to generate a test stub for your application.js file, use the generator like this:

script/generate javascript_test application

That command will generate a new JavaScript unit test stub at test/javascript/application_test.html. The file looks like this:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">   <head>     <title>JavaScript unit test file</title>     <meta http-equiv="content-type"        content="text/html; charset=utf-8" />     <script src="/books/4/386/1/html/2/assets/prototype.js"        type="text/javascript"></script>     <script src="/books/4/386/1/html/2/assets/unittest.js"         type="text/javascript"></script>     <script src="/books/4/386/1/html/2/../../public/javascripts/application.js"         type="text/javascript"></script>     <link rel="stylesheet" href="assets/unittest.css"         type="text/css" />   </head>   <body>     <div >       <div >         <h1>JavaScript unit test file</h1>         <p>This file tests <strong>application.js</strong>.</p>       </div>       <!-- Log output -->       <div > </div>     </div>     <script type="text/javascript">       new Test.Unit.Runner({              // replace this with your real tests              setup: function(  ) {                },              teardown: function(  ) {                },              testTruth: function(  ) { with(this) {           assert(true);         }}              }, "testlog");     </script>   </body> </html>

In this example, the head element takes care of including any needed JavaScript files: prototype.js and unittest.js, as well as application.js (where the application-specific code resides).

The good stuff starts toward the end, with Test.Unit.Runnerscript.aculo.us' unit testing container. Here, we see three methods. The setup method is run before every test case and can be used to create a blank slate, setting up objects for the tests to interact with. The counterpart to setup is teardown; it's called after each test case, and it can be used to clean things up, if needed. The third method is a trivial test case that will always pass.

It's easy to run the tests from the command line:

rake test:javascripts

Impressively, the plug-in will scan your system for available browsers, run the JavaScript tests in each browser, and report the results back on the command line. The browser windows have to be closed manually, but you can see the results of the test run there, as seen in Figure 7-7.

Figure 7-7. JavaScript unit rest results


Let's take a look at a practical example of a JavaScript unit test. Here's a small snippet taken from the Review Quiz example application, which we'll create a test for:

var Quiz = {   /* Reveals the answer node for a question */   reveal: function(questionId) {     $(questionId+'_a').visualEffect('blind_down', {duration:0.25})   } }

The code is simple: the static method Quiz.reveal( ) takes one argument and creates a visual effect based on that argument. The actual application has several more methods in the Quiz object, but, for this example, we'll just test reveal( ). The first job is to add a JavaScript include for effects.js, since our Quiz.reveal( ) method uses a visual effect:

<script src="/books/4/386/1/html/2/../../public/javascripts/effects.js"    type="text/javascript"></script>

Next we'll add a DOM element to the page, for the code to interact with:

<div > </div>

And finally, the test itself:

new Test.Unit.Runner({   setup: function(  ) {     $('sandbox').innerHTML =        "<div id='123_a' style='display:none;'></div";   },   testQuizReveal: function(  ) {with(this) {     assertHidden($('123_a'));     Quiz.reveal('123');     wait(500, function(  ){        assertVisible($('123_a'));     });   }} }, 'testlog');

The testQuizReveal method contains the meat. First, it asserts that the starting condition is correct (the element is hidden). Then it calls the method being tested. Finally (after a brief wait to allow the visual effect to finish), it asserts that the ending condition is correct (the element is visible).

Just as with Rails unit tests, JavaScript unit tests aren't written to be thrown away. As your application's JavaScript continues to grow and evolve, your tests just become more valuable, helping to ensure that new changes don't break old functionality.

And because JavaScript unit tests are run in the browser, they serve another important purpose: they can be used to verify that your application works across platforms. Instead of verifying each by hand on every platform, just load one test file and let the tests do the work for you. With a thorough suite of unit tests on hand, you'll have little reason to worry when a new version of a browser is releasedjust run your tests on the new platform and be assured that it hasn't broken any of your code's assumptions.

We've hardly scratched the surface of what's possible with unittest.js. For more inspiration, take a look at the Prototype and script.aculo.us distributions themselvesthey're both backed by extensive unittest.js test suites. For more information about the assertions you can use within your tests, see the script.aculo.us wiki: http://wiki.script.aculo.us/scriptaculous/show/Test.Unit.Assertions.




Ajax on Rails
Ajax on Rails
ISBN: 0596527446
EAN: 2147483647
Year: 2006
Pages: 103
Authors: Scott Raymond

Similar book on Amazon

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net