Skip to main content

skip to main content

developerWorks  >  Web development  >

Real world Rails, Part 4: Testing strategies in Ruby on Rails

Choosing the right tools and techniques for your Rails team

developerWorks
Document options

Document options requiring JavaScript are not displayed

Discuss


Rate this page

Help us improve this content


Level: Intermediate

Bruce Tate (bruce@rapidred.com), CTO, RapidRed

14 Aug 2007

Testing is firmly entrenched in the Ruby on Rails community. Many tools can help you, from the Rails stack to RCov for coverage to Mocha and FlexMock for enhancing your test cases. But different tools often support diverging strategies. Learn about the trade-offs of several basic testing strategies.

One of the most distinctive aspects of the Rails platform is the Ruby language itself. As a dynamically typed language, Ruby has great flexibility, convenience, and power, but at a cost. Dynamically typed languages do not have a compiler that can catch certain kinds of errors, including relatively common type errors and some misspellings. Very early on, users of dynamically typed object-oriented languages learned that they had to test.

The Ruby on Rails community embraces testing like the USA embraces American Idol. They watch their test case results roll by with great regularity. Ruby developers talk about testing; they blog about it; they even participate behind the scenes, not with cell phone votes but by contributing open source frameworks.

Without testing, Ruby applications would have a much higher incidence of error per line of code than they do. With testing, you can have the benefits of dynamically typed languages with much less of the downside. In this article, I won't bore you with basic decisions, like whether you should test at all, or how you can convince your managers that the testing effort is worthwhile. I'll assume you already test. Instead, I'll break down some of the more subtle testing decisions that every Ruby project lead must eventually make. I'll talk about how you can measure your testing coverage, and how much testing you should do. I'll walk you through the basic out-of-the-box testing techniques, and how those compare with the newer mocking frameworks. Rather than providing an exhaustive tutorial, I'll give you a few examples of the techniques we used to build ChangingThePresent.org (see Resources) so you'll have a basic flavor of the techniques in play. Then I'll break through the strengths and weaknesses of the various techniques as we see them.

Rails testing, out of the box

The Rails framework has surprisingly robust testing before you add a single gem. With minimal effort, you can specify a repeatable database setup, send simulated HTTP messages to your Web applications, and do three kinds of tests: unit tests, functional tests, and integration tests. The following section provides a brief example of each.

Unit tests

Unit tests exercise Rails model code and sometimes helpers. Your unit tests will make sure your model does what you've built it to do and that the associations in your model behave as you think they should. As you recall, Rails models are objects that wrap a single database table. For the most part, each database column is an attribute on the model. Rails helpers are functions that help to simplify model, view, or controller code. You need to make sure that each model or helper has a test. At ChangingThePresent, our unit tests for the most basic models are very thin.


Listing 1: A basic model test
                
require File.dirname(__FILE__) + '/../test_helper'

class BannerStyleTest < Test::Unit::TestCase
  fixtures :banner_styles

  def test_associations
    assert_working_associations
  end

  def test_validation_with_incorrect_specs_should_fail
    bs =  BannerStyle.new(:height => 10, :width => 10, :format => 'vertical_rectangle',
                          :model_name => 'Nonprofit')
    assert !bs.save, bs.errors.inspect

    bs2 =  BannerStyle.new(:height => 400, :width => 240, :format => 'vertical_rectangle',
                          :model_name => 'Nonprofit')
    assert bs2.save, bs2.errors.inspect
  end

  ...
end
 

In Listing 1, you see a condensed test case with two tests. Banner styles create simple advertising banners. The sizes and shapes of each are based on a loose set of standards. The application uses a table of standards to ensure that any new banner conforms to the standards. The first test uses a helper to exercise all of the associations on BannerStyle through reflection, as in Listing 2. The second test makes sure that a banner with an incorrect height and width cannot be saved, and that the model correctly saves a banner with valid specs.


Listing 2. A helper to test working associations
                
def assert_working_associations(m=nil)
  m ||= self.class.to_s.sub(/Test$/, '').constantize
  @m = m.new
  m.reflect_on_all_associations.each do |assoc|
    assert_nothing_raised("#{assoc.name} caused an error") do
      @m.send(assoc.name, true)
    end
  end
  true
end
 

Listing 2 shows the helper that exercises all of the associations on a class. The assert_working_associations method simply walks through all of the associations on the class and sends the model the name of the association. This catch-all ensures that with a single line of code per model, I can invoke all relationships on all of our model tests.

Functional and integration tests

Functional tests exercise the user interface through isolated HTTP requests. The Rails framework makes it easy to invoke single HTTP GET and POST commands, forming the backbone of the tests. Integration tests are the same, but they can invoke many HTTP requests back to back. The principle and structure of the tests is the same. Listing 3 shows a few basic functional tests.


Listing 3. A simple functional test
                
require File.dirname(__FILE__) + '/../test_helper'
require 'causes_controller'

class CausesController; def rescue_action(e) raise e end; end

class CausesControllerTest < Test::Unit::TestCase
  fixtures :causes, :members, :quotes, :cause_images, :blogs, :blog_memberships

  def setup
    @controller = CausesController.new
    @request    = ActionController::TestRequest.new
    @response   = ActionController::TestResponse.new
  end

  def test_index
    get :index

    assert_response :success
    assert_template 'index'

    assert_not_nil assigns(:causes)
    assert_equal Cause.find_all_ordered.size, assigns(:causes).size
  end

  def test_should_create_blog
    assert Cause.find(2).blog.nil?
    get :create_blog, :id => 2
    assert Cause.find(2).blog.nil?

    login_as :bruce
    get :create_blog, :id => 2
    assert !Cause.find(2).blog.nil?
    assert_equal Cause.find(2).name, Cause.find(2).blog.title
  end
 

In Listing 3, you can see that the interactions between the test and the system are all through HTTP GETs and POSTs. The basic flow of a test is the following:

  1. Issue a simple HTTP operation.
  2. Test the impact of the HTTP operation on the system.

In addition, Listing 3's setup method sets up testing scaffolding to simulate HTTP calls. The testing scaffolding removes requirements for networking and infrastructure, isolating the test cases to the application itself.

Stubs

For ChangingThePresent.org, we added a few test helper methods that made it easy to do things like log in. You can see the login_as :bruce method call in the fifth line of the test_should_create_blog method in Listing 3. That invokes the helper in Listing 4, which stubs out the site login feature, copying the member's ID directly to the session. If you've used Rails acts_as_authenticated, you know that a logged-in user will set the value associated with the :user key within the session.


Listing 4. Stubbing out login
                
def login_as(member)
  @request.session[:user] = member ? members(member).id : nil
end
 

Many developers confuse the ideas behind stubbing and mocking. Stubbing simply replaces a real-world implementation with a simpler implementation. In Listing 4, the stub replaced our full login system with a simple substitute. A stub's job is to simulate the real world. Mocks are not stubs. A mock object, instead, is like a gauge that measures the way your application uses an interface. I'll discuss stubs in more detail later and show you a few examples.



Back to top


Basic concepts

Now you've seen the essence of the Rails out-of-the-box testing experience. Before I go much further, I should outline a couple of core decisions: How much, and how fast? As you build an overall testing philosophy, you will want to pay careful attention to the trade-offs surrounding coverage and speed.

Coverage

One of the most critical decisions you'll ever make is how much you test. If you don't test enough, you'll compromise your code quality and often even slow down your delivery time. You can test too much, too. Test too much, and you'll possibly miss deadlines that are important to the business. To make an informed decision about how much to test, you need to accurately measure how much you're already testing. One of the most critical measurements for testing is code coverage.

For ChangingThePresent, we use RCov to determine test coverage. I can run the traditional rake command and get the traditional trail of dots as a report. I can also run rake test:coverage to get a more complete report that looks like the one in Listing 5.


Listing 5. Running rake test:coverage with RCov
                
807 tests, 2989 assertions, 0 failures, 0 errors
+----------------------------------------------------+-------+-------+--------+
|                  File                              | Lines |  LOC  |  COV   |
+----------------------------------------------------+-------+-------+--------+
|app/controllers/address_book_controller.rb          |   142 |   123 |  84.6% |
|app/controllers/admin_controller.rb                 |    77 |    65 |  93.8% |
|app/controllers/advisor_admin_controller.rb         |    86 |    63 |  88.9% |
|app/controllers/advisors_controller.rb              |    52 |    42 | 100.0% |

...


|app/models/stupid_gift.rb                           |    56 |    45 | 100.0% |
|app/models/stupid_gift_image.rb                     |    10 |    10 | 100.0% |
|app/models/titled_comment.rb                        |     2 |     2 | 100.0% |
|app/models/upgrade.rb                               |    13 |    10 | 100.0% |
|app/models/upgrade_item.rb                          |     3 |     3 | 100.0% |
|app/models/validation_model.rb                      |     7 |     7 | 100.0% |
|app/models/volunteer_opportunity.rb                 |   137 |   129 |  93.0% |
|app/models/work_period.rb                           |     5 |     4 | 100.0% |
+----------------------------------------------------+-------+-------+--------+
|Total                                               | 12044 | 10044 |  81.8% |
+----------------------------------------------------+-------+-------+--------+
81.8%   167 file(s)   12044 Lines   10044 LOC
 

RCov takes a lot longer to run (just under twice as log for our tests), so I don't run that command all of the time, but when I do, I can tell exactly how much test coverage that I have on any given file. Better still, I can open a coverage file in my browser and see exactly which lines of code my test cases cover. Figure 1 shows an example of a typical coverage report.


Figure 1. An actual RCov report
Figure 1. An actual RCov report

When you have numbers, you can start to make some ballpark decisions about exactly how much you will test. For ChangingThePresent, we've had fluctuating test coverage statistics, but we're settling on a number between 80% and 85%. As new major features are under development, the coverage will temporarily decrease. As we bring those new features online, our coverage will increase. Presently, the coverage sits at 81.7%.

Keep in mind that our answer may not ultimately be the same as yours. Test coverage will vary based on the experience of the developers on your team, the complexity of your application, the tolerance of your application for errors, and the tolerance of your business for delay. If you are building an airplane engineering application, you're going to need more tests; if you're building a fad Web 2.0 application for Facebook that will be worthless two months from now unless you hit a market window, you'll need to test less. The best Ruby programmers I know all suggest coverage of production code of over 80%, and some strive for 100% coverage.

Even if you do achieve 100% coverage, you have no guarantee that your tests are any good at all. You have to also consider the types of tests, with both happy paths and boundary conditions, to have the best possible coverage.

Now that you have the tools to understand how much to test, you can switch gears and address testing speed. And with Rails, the database will determine the speed of your tests. Traditional attempts at database-backed test tools are riddled with problems. The two biggest problems are repeatability and speed. From a repeatability standpoint, it's hard to build a good test suite without changing the database, but changing the database also changes the test data, which in turn changes the behavior of your tests. The second problem is speed. Changing the database is expensive.

Speed

As you know, the Rails environment solves the repeatability problem with fixtures. Each developer sets up fixtures with test data. Before each test case, the Rails testing frameworks will completely erase each model's data and load each fixture that you specify for each test case. Each test case can then start with a clean slate. But each test case has multiple individual tests, and each one should be totally independent of the others. Loading the whole set of fixtures for each individual test would be way too slow.

Rails partially solves the speed problem with a brilliant compromise. After running each test case, Rails rolls back all database changes. A rollback is much faster than loading all fixture data from scratch. Still, you can't deny the cost of database access. Even with rollbacks, database-backed testing is slow. And if tests are too slow, your developers will not run them. If they don't get run, they are practically useless. Though Rails solves the repeatability issue, it cannot completely solve the speed issue. The speed of running tests will shape testing strategies for years to come.

One alternative is to use an in-memory database for your tests. Usually, SQLite will run much faster than MySQL. The downside is that you will probably not test on the same platform as your production system.

If you're using ActiveRecord for your database backing, you will likely do all of your unit tests with database-backed testing. You'll accept the low speed as a cost of development. But there's no rule that says you have to use database-backed models to do your functional tests. Many Rails developers now use stubs or mocks to take the database out of the picture, forming lightning-fast functional tests.



Back to top


Mocking and stubbing with Mocha and FlexMock

Earlier, I explained that stubbing is a technique that replaces a real-world implementation with a simpler implementation. Test cases may use stubbing to make an implementation simpler, faster, or more predictable. For example, you might want a system clock to always return the same time so that your test has repeatable results that you can verify.

The Mocha framework makes stubbing easy. You just define the result that you expect. The following code will stub the system class Date to always return the same date, say Ground Hog Day, as in Listing 6.


Listing 6. Creating a simple stub
                
ground_hog_day = Date.new(2007, 2, 2)
Date.stubs(:today).returns(ground_hog_day)
assert_equal 2, Date.today.day
 

If a stub provides a simplified simulation of the real world, a mock does more. Sometimes, simply aping the real world is not enough. When you test, you want to make sure that your code uses an API correctly. For example, you might want to verify that a native database application opened a connection, executed a query, and then closed the connection. You might want to verify that a controller actually calls save on a model object. So a mock object must establish expectations, as well as behavior.

Rails has at least three mocking libraries getting at least some play: Mocha, FlexMock, and RSpec. I'm going to focus on Mocha, but each of the three has its own set of advantages. Using Mocha, you actually spell out each expected API call, followed by the result Mocha should return, as in Listing 7.


Listing 7. The Mocha mocking library
                
mock_member = mock("member_67")
mock_member.expects(:valid?).returns(true)
mock_member.expects(:save).returns(true)
mock_member.expects(:valid_captcha=).with(true)
mock_member.expects(:plaintext_password).returns('password')
mock_member.expects(:id).returns(67)
Member.expects(:find_by_login).with(nil).returns(mock_member)

post :create, :member => {}, :nonprofit => {:id => 67}

...

assert_response :redirect
assert_redirected_to :controller => 'nonprofits', :action => 'show',
 :id => mock_nonprofit_id
 

Listing 7 shows an example test case for creating a new member. I can establish my expectations for each interaction between the controller and the mock user. I create a mock member and define each interaction independently. Next, I actually mock the Member class and mock a finder that returns mock_member.

You can see that the interaction with the model layer is fairly involved, but the mock object will completely insulate the member behavior from the functional test case. This has a couple of immediate advantages. Using certain APIs, like credit card checkouts and self-destruct switches is not practical. Other APIs, like time- or memory-based services, are not predictable enough. You will almost always use mock object frameworks to mock or stub them.

The more interesting discussion deals with whether to mock or stub your database-backed model. One upside is speed: This test case will not hit the database at all. Another is independence. I completely isolate the code under test to the controller layer. But you can probably notice a few downsides, too. I do complicate my test cases tremendously. I also force rippling changes whenever I change the behavior of my models, because I must change the model object and the test cases that surround them. I can make it easy for a test case to miss something vital. Adding a single validation could break an important scenario, but go completely unnoticed. For this reason, for ChangingThePresent, we do not mock model object classes. We limit the mocking to external interfaces, such as Web services to third parties or network services.

I should point out that the Ruby community is heavily leaning in the direction of the mocking strategies. My shop is definitely cutting against the grain with our decision to stay database-backed. We've tried both. We use these techniques because they work better for us and our code base.



Back to top


Continuous integration

One of the most important enhancements we've added to our overall discipline is continuous integration (CI). We run the Ruby version of Cruise Control. Our CI server checks out a clean build and runs test cases from scratch each time I check in a new change. This server notifies each developer whenever a change breaks the build. This server allows me to run a few representative tests before checking in. I might change a few lines in Member, and then run unit/member_test.rb and functional/members_controller_test.rb. Fifteen seconds later, I can check in, confident that Cruise Control will shout out to me if anything is amiss.

Wrapping up

A few years ago, the interesting debates surrounding testing seemed to revolve around whether automated testing was a good idea. Now, the debates are far more interesting:

  • Should all of your tests be database-backed, or should you use mocks and stubs to isolate functional tests?
  • Is 100% coverage a realistic goal?
  • Does the additional speed of an in-memory test offset the additional risk?

My best advice to you is to pick techniques for your team based on what works for you, and for your customers. Don't let some expert talk you into something that feels wrong. As always, over the next few years, we will find that we did not, in fact, have all of the answers. New techniques will emerge, and existing ones will fall out of favor. What happens on paper does not always perfectly mirror real-world Rails.



Resources

Learn
  • ChangingThePresent.org is the portal for donors and nonprofits that forms the basis of this article. You can give a donation gift — an hour of a cancer researcher's time or making a blind person see — to honor those you love.

  • Learn more about continuous integration with CruiseControl.rb. The ThoughtWorks continuous integration server has saved us countless hours of pain.

  • Mocks are not stubs: Martin Fowler's timeless classic walks you through the difference between stubs and mock objects.

  • FlexMock is the Ruby mock framework that seems to be in favor today.

  • Mocha is the mocking framework that we use for ChangingThePresent.

  • RCov is a great test coverage tool for Ruby.

  • Get your hands on more howto articles from the Web development zone's technical library.

  • Subscribe to the developerWorks Web development newsletter.

Get products and technologies

Discuss


About the author

Bruce Tate

Bruce Tate is a father, mountain biker, and kayaker in Austin, Texas. The CTO of WellGood, LLC and the chief architect behind ChangingThePresent.org, he's also the author of nine books, including Beyond Java, From Java to Ruby, and Ruby on Rails: Up and Running. He spent 13 years at IBM and later formed the RapidRed consultancy, where he specialized in lightweight development strategies and architectures based on Ruby, and in the Ruby on Rails framework. He now works with a team of Rails developers to build and maintain the charity portal, ChangingThePresent.org.




Rate this page


Please take a moment to complete this form to help us better serve you.



YesNoDon't know
 


 


12345
Not
useful
Extremely
useful
 


Back to top