Quantcast
Channel: Building Web Apps
Viewing all articles
Browse latest Browse all 27

Lab 6. Testing

$
0
0

In this lab, we are going to look at some examples of unit and functional testing in Rails. Many of the tests that Rails generates for us automatically need some fixing up to track the changes we made in the code (such as adding authentication), and in this lab we’ll walk through those changes. Then we’ll take some first steps toward more complete tests.

As we’ve discussed, there are multiple approaches to writing tests. In this lab, we are writing tests after we’ve created logic in our program. We encourage you to take a look at test-driven development as well.

Creating automated tests for a Rails application is easier than with most other technologies, but it is still a relatively complex topic. If you’re early in your Rails learning curve, you many want to skip this material and return to it later.

1. Running tests

Many generator scripts, such the scaffold generator, create placeholder test files at the same time they generate the associated application code. All of these test source files get placed in the test directory hierarchy of the application.

Let’s start by seeing what happens when we run our tests:

rake test

You can run more focused sets of the tests by selecting rake test:units or rake test:functionals for instance.

You can also trigger the tests within NetBeans too: just right-click on the project, select the Run Rake Task menu, and select one of the test options.

Running our tests, we get a lot of feedback. Rake runs the tests in sequence, first the unit tests, then the functionals, and so on.

It looks like we have some work to do.

2. Reading the test results

Let’s take apart the results a little at a time. For the unit tests, we scan down and see the lines:

	/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -Ilib:test "/Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader.rb""test/unit/asset_test.rb""test/unit/category_test.rb""test/unit/contact_mailer_test.rb""test/unit/contact_test.rb""test/unit/content_block_test.rb""test/unit/customer_test.rb""test/unit/link_test.rb""test/unit/subscription_test.rb""test/unit/user_test.rb" 
	Loaded suite /Library/Ruby/Gems/1.8/gems/rake-0.8.1/lib/rake/rake_test_loader
	Started
	..E..................
	Finished in 0.19429 seconds.	

This block of information tells me what tests file were loaded and run, and using the simple command line result format, we see a series of periods and an “E”.

Recall that tests can have one of three states:

  1. Success, indicated with a period (.), sometimes also called “green”;
  2. Failure, indicated with an F, (called “red”), which means one of the assertions we wrote to test an assumption about our code did not work;
  3. Error, indicated by an E, (also called “red”), which means a program logic problem was detected in our test or application code.

So, apparently, we have a number successful tests from seeing all of those periods, but one errored out.

We look further down in the unit test results and see:

   1) Error:
   test_contact_form(ContactMailerTest):
   ArgumentError: wrong number of arguments (1 for 4)
	    /Library/Ruby/Gems/1.8/gems/actionmailer-2.0.2/lib/action_mailer/base.rb:410:in `contact_form'
	    /Library/Ruby/Gems/1.8/gems/actionmailer-2.0.2/lib/action_mailer/base.rb:410:in `__send__'
	    /Library/Ruby/Gems/1.8/gems/actionmailer-2.0.2/lib/action_mailer/base.rb:410:in `create!'
	    /Library/Ruby/Gems/1.8/gems/actionmailer-2.0.2/lib/action_mailer/base.rb:403:in `initialize'
	    /Library/Ruby/Gems/1.8/gems/actionmailer-2.0.2/lib/action_mailer/base.rb:351:in `new'
	    /Library/Ruby/Gems/1.8/gems/actionmailer-2.0.2/lib/action_mailer/base.rb:351:in `method_missing'
	    ./test/unit/contact_mailer_test.rb:10:in `test_contact_form'
	    /Library/Ruby/Gems/1.8/gems/activesupport-2.0.2/lib/active_support/testing/default.rb:7:in `run'

This gives us a clue as to what went wrong and where. The ContactMailerTest class’ test_contact_form method had a programming error in it: an incorrect number of parameters were passed in. We’ll look at that in a minute.

Finally, just below the list of diagnostic messages for failing or error state tests, we get the summary:

   21 tests, 33 assertions, 0 failures, 1 errors

Wow, we had 21 tests with 33 assertion calls, and we didn’t write any code. Before we look at the problem in ContactMailerTest, what were all of the successful tests doing?

Recall that unit tests check out our models. If we look in all but ContactMailerTest and UserTest, we see classes that contain this:

   # Replace this with your real tests.
   def test_truth
      assert true
   end

Alas, this is a do-nothing placeholder method, so we will ignore it for now. What we should do is write some tests to exercise each model. We’ll try that later.

Peeking inside UserModel, we see a different story. This class is full of tests. It turns out this test file was generated for us when we used the restful_authentication plugin and associated generator to create our user and login system. UserModel tests most of the functionality of our User model and is a good example of how a model should be tested.

3. Fixing the Unit Tests

For now, though, let’s fix our one error in ContactModelTest. This test is exercising the pseudo model that was created when we generated our contact mailer. The test inside of the test class looks like this:

  def test_contact_form
    @expected.subject = 'ContactMailer#contact_form'
    @expected.body    = read_fixture('contact_form')
    @expected.date    = Time.now

    assert_equal @expected.encoded, ContactMailer.create_contact_form(@expected.date).encoded
  end

Mailer related unit tests are a little strange. What this code is trying to do is test to see if the mail message that gets generated by the ContactMailer.contact_form method is identical to a canned email fixture. @expected is an instance variable created for us by the testing class that embodies a TMailer object and which we’ll load with data and subsequently ask to generate a properly formed email body and headers (that is what encoded does).

Here we see that the other special calling convention on a mailer is being used: create_contact_form. Like deliver_contact_form, this method uses the contact_form method we wrote, but rather than building a mail message and then actually emailing it out, this form just returns a string that contains the body of the email message.

To fix this test, we need to make the generated email body equal to the canned version of the email message we prepare in the @expected instance variable. Let’s do that.

a. Call contact_form correctly

The error we received in the first place indicated that we are calling the underlying contact_form incorrectly (not enough parameters). We need to supply name, email, subject, message, and a sent_at time into that method:

  assert_equal @expected.encoded, ContactMailer.create_contact_form('bob', 'bob@bob.com', 'test', 'testing', @expected.date).encoded

If we run the test again (rake test:units), we see the Error is gone, but now we have a Failure: the messages are not equal. Let’s fix that.

b. Set up fixture data

First, lets create a fixture data file with the general body of the email message. This needs to match the view we’ve created to contain the message body (in contact_form.erb from the previous lab):

   Email from your web site

   From: <%= @name %>

   Message: <%= @message %>	

Our fixture (found in the directory test/fixtures/contact_mailer/contact_form, another peculiar convention for mailers) looks like this:

   Email from your web site

   From: bob

   Message: testing

Note that we use hard-coded values in the fixture to make it an exemplar of a known working message body.

We are closer, but this still fails. We need to set up the rest of the data needed for the encoded message we are using as our “correct” email state.

c. Fill in rest of email data

We encode the rest of the values in test_contact_form's @expected variable with the sample data we choose to pass in to our own contact_form call:

   def test_contact_form
      @expected.subject = 'test'
      @expected.body    = read_fixture('contact_form')
      @expected.date    = Time.now
      @expected.to      = CONTACT_RECIPIENT
      @expected.from    = 'bob@bob.com'
    
      assert_equal @expected.encoded, ContactMailer.create_contact_form('bob', 'bob@bob.com', 'test', 'testing', @expected.date).encoded   
   end

We save and run this now, and Success! Our admittedly mostly empty unit tests are all running in the “green” state. Let’s go on to fix up some of our functional tests.

4. Fixing the Functional Tests

We now know how to read the test results, so switching our attention to the functional test block results, we see we have our work cut out for ourselves:

   FFFFFFFFFFFFFFEFFF.FFFFFFFFFF......FFFFFFF..........F......FEEEE

Which in my first run was equivalent to 64 tests, 76 assertions, 36 failures, 5 errors. Ouch.

The Rails scaffold generator does a much better job at creating functional tests than it does with unit tests. Functional tests exercise our controllers. Peeking inside most of the test files in the test/functionals directory, you will note that they follow similar patterns.

So what went wrong? In scanning through the test diagnostics, we see a lot of tests with similar issues. There are many tests with problems with redirects: “Expected response to be a <:success>, but was <302>”. There are many tests where things were not created, updated, or destroyed when expected. Thinking back, the common thread here is that all of these things need you to be logged in, yet, when the tests are automatically generated for us, the generator has no idea about this requirement. Let’s fix that. h3. a. Adding authentication to existing tests

The restful_authentication plugin that we used provides a utility module called AuthenticatedTestHelper. This module contains useful methods that you can mix-in to your test classes to simulate things like logging in, precisely what we need. Since we protected most of our administrative functions with those before_filter :login_required checks, that will cause any of our tests that try to directly exercise an action to instead get redirected to a login screen.

Let’s pick one of our controller’s tests to fix up as our example. The others will follow similar patterns and you can look at the final lab code to see the changes (or try to make the changes yourself as an exercise).

First, add the authentication system’s helper at the top of the test class. While there, add the user fixtures so we have some test user accounts to log in with:

   class ContentBlocksControllerTest < ActionController::TestCase
      include AuthenticatedTestHelper
      fixtures :users

Now, let’s use the new method we have to log in as the user quentin:

   def test_should_get_index
      login_as :quentin
      get :index
      assert_response :success
      assert_not_nil assigns(:content_blocks)
   end

Run the tests again. The redirect failure on test_should_get_index should now be gone.

Go back to each method in this test class and add the login_as :quentin line at the start of each test method. All of your redirect failures should now be cleaned up.

But, there are still failures to fix. Look through the other functional tests and make the same changes to all of the others that are protected with the before_filter (assets_controller_test, categories_controller_test, contacts_controller_test, links_controller_test).

For the controllers that are selective about the login requirement, be sure to not login when it isn’t needed. For instance, contacts_controller_test’s new and create tests are not protected by login. See the controller to confirm this.

Run the tests again. We are getting closer. In my run, the results look like this:

   Started
   FE...EE.......E.............F.......................F......FEEEE
   Finished in 6.451664 seconds.

b. Testing asset uploader

Our asset_controller_test is responsible for a number of those errors and failures, so let’s clean it up next. Recall we are using the attachment_fu plugin to implement our uploader, and as luck would have it, the associated README has some guidance about implementation, but not about testing. We’ll have to dig in to the code to figure that out. We’ve done that for you, so let’s walk through what we need to do.

First, make sure you have a test image file as an asset to play with. This takes the role of a fixture, so make a directory in the test/fixtures directory called “files” and place a test file in there. We used an image called “railsquickstart-train.gif”; you can use your own if you wish.

Next, we add information we may need later in our regular fixture file, assets.yml, which spells out some sample content. This will fix any issues with preloaded data for our Asset model, since the validates_as_attachment call makes sure the minimum needed data for a legal attachment is present:

	one:
	    name: first
	    content_type: 'image/gif'
	    filename: '/files/railsquickstart-train.gif'
	two:
	    name: second
	    content_type: 'image/gif'
	    filename: '/files/railsquickstart-train.gif'

Just a few more things. Recall that asset uploading is done through a multipart form upload. The default RESTful controller tests that are created for us by the generate script do not know about the multipart form technique, so we need to help things along.

In our test_should_create_asset method, we need to use some testing helpers to simulate the file upload and we need to tell the post method that we want to do a multipart form. Cruising the documentation on BuildingWebApps.com, we find the fixture_file_upload method, and drop that in:

  def test_should_create_asset
    login_as :quentin
    image_file = fixture_file_upload('/files/railsquickstart-train.gif', 'image/gif')
    assert_difference('Asset.count') do
      post :create, :asset => { :name => 'railslogo', :uploaded_data => image_file },
                    :html => {:multipart => true }
    end

    assert_redirected_to asset_path(assigns(:asset))
  end	

We also know that our update method is going to have the same issue, so let’s fix that at the same time:

  def test_should_update_asset
    login_as :quentin
    image_file = fixture_file_upload('/files/railsquickstart-train.gif', 'image/gif')
    put :update, {:id => assets(:one).id, :asset => { :uploaded_data => image_file }}
    assert_redirected_to asset_path(assigns(:asset))
  end	

Run the tests. Indeed, we’ve cleaned up the asset_controller tests.

c. User controller cleanup

We see there are still some issues around the user_controller_test. This was a test file that was generated for us when we used the restful_authentication plugin, so what’s up?

We added a before_filter in the users_controller too, to protect the creation of accounts. Let’s fix that up quickly just like we did earlier. Add the AuthenticatedSystemHelper and use the login_as methods in the tests.

d. Email bits and pieces

Down to the wire, and it looks like we have just a few tests to fix up.

The first appears to be an error in test_should_create_contact(ContactsControllerTest). It can’t seem to locate a constant ContactMailer::CONTACT_RECIPIENT. This is an error, so that means some kind of programming mistake is the cause. If you search the code with your editor, you’ll see that CONTACT_RECIPIENT is indeed defined, but it is in config/environments/development.rb.

Recall that testing runs in its own, completely separate, environment. Here is a case where a constant defined in the environment is just plain missing in the test environment. This is fixed by adding (or copying from development.rb) the CONTACT_RECIPIENT definition to the config/environments/test.rb file at the bottom:

   CONTACT_RECIPIENT = 'tester@buildingwebapps.com'	

Unless you have a working email connection, you’ll also want to add a line to ignore errors when trying to connect to the mail system (below the CONTACT_RECIPIENT line is ok):

   config.action_mailer.raise_delivery_errors = false

Now, this is somewhat of a hack. We put mail configuration into the initializer file config/initializers/mail.rb. Initializers run after the environment is loaded, and since we are globally setting up the sendmail environment there, we actually have our delivery_method setting stepped on and changed from :test to :sendmail.

In retrospect, we probably should not have put the setup code in the mail.rb initializer file, and instead put the mail setup code in the environment-specific files. That way, we can get the test code to act properly without ignoring possible exceptions.

e. Customers use validation

In this lab, we don’t have a lot of detailed testing for our merchant-oriented code. Even so, we still have a couple of failures to clean up.

test_should_create_customer fails with a pattern that will become familiar to you as you test code that uses validations. Here, the code is trying to make a new customer object. The test, however, by default tries to create an empty customer. Looking inside of the Customer model, we see a validates_presence_of on :first_name, :last_name, :address, :city, :state, and :zip. We better fill in some values for those:

  def test_should_create_customer
    assert_difference('Customer.count') do
      post :create, :customer => { :first_name => customers(:one).first_name,
                                    :last_name => customers(:one).last_name,
                                    :address => customers(:one).address,
                                    :city => customers(:one).city,
                                    :state => customers(:one).state,
                                    :zip => customers(:one).zip }
    end

    assert_redirected_to customer_path(assigns(:customer))
  end

But, what is this customers(:one) syntax? Those aren’t straight string constants. No, instead, we are using references to data we are storing in our fixtures file. Here is what is going on. First, we need the test controller class to load the fixture data, so we add the fixtures :customers line right after the class declaration:

   ...
   class CustomersControllerTest < ActionController::TestCase
      fixtures :customers

      def test_should_get_index
      ...

Now, in our customers.yml fixtures file, we make sure there is some data:

one:
  first_name: 'First'
  last_name: 'Last'
  address: 'Street'
  city: 'City'
  state: 'State'
  zip: 11111
  phone: '123-456-5678'

As a reminder, YAML files are picky about spacing. The label one: starts in the first column and its attributes (e.g. first_name:) are all tab indented.

When loaded, fixtures can be referenced by name, using the name of the file (“customers”), then in parentheses, the name of the record we want as a symbol (e.g., one: is the name in the fixtures file, but in code, we use the symbol :one; note the way that colon moved!). We are now referring to a loaded instance of a model object, so any data field defined in that model is available to us, referred to by a period followed by the attribute name (e.g., customers(:one).first_name). Now our test data and our test code are each in separate files, which makes it easier to maintain them in the future.

f. Subscriptions are compound objects

Subscriptions have a similar problem but are a bit more complex. The ActiveMerchant code and the logic to stitch together subscriptions, customers, and credit cards involves a dance of three models and some business logic. Our remaining error is in the create method, which is the most complex in the controller. Let’s supply all of the data it needs:

	def test_should_create_subscription
    assert_difference('Subscription.count') do
      post :create, :subscription => { :amount => subscriptions(:one).amount,
                                        :period => subscriptions(:one).period },
                    :customer => { :first_name => customers(:one).first_name,
                                    :last_name => customers(:one).last_name,
                                    :address => customers(:one).address,
                                    :city => customers(:one).city,
                                    :state => customers(:one).state,
                                    :zip => customers(:one).zip }, 
                    :creditcard => { :number => '4111111111111111', 
                                    :type => 'visa', 
                                    :month => '10', :year => '2010', 
                                    :verification_value => '999'}
    end

    assert_redirected_to root_path
  end

Wow, that’s a lot!. Here is what we are doing (using a few different techniques for illustration). First, we are supplying a subscription object. The Subscription model requires a value “amount”, so we pull one from our fixtures (defined at the top of our file as usual with fixtures :customers, :subscriptions since we are also using the customers data).

Next, we need to provide a Customer model object, so we reuse the same fixture data we used earlier for the Customer tests.

Lastly, we need to supply a CreditCard object, as used by ActiveMerchant. We should put that data into its own fixture, but here we spell it out explicitly to show that you can do that, if you need or want to.

The create method is now happy, receiving the three model objects it will need. On success, however, it doesn’t send the user back to a show method, like many RESTful controllers, but rather redirects the user to the home page. We adjust our assertion to point to the root_path, which is defined in our routes.rb file as a named route for “home page”.

Run the tests. Success! No problems.

g. Aside: Write a new functional test

We lost a valuable piece of information when we fixed all of those authentication tests. We really should also test that the right thing happens when someone is not logged in. Let’s write a quick example of a test that succeeds when an unauthenticated person tries to get a protected resource. In ContentBlocksControllerTest, add this test:

   def test_should_not_get_index_not_logged_in
      get :index
      assert_redirected_to new_session_path
   end

Here, we are not logged in, and we are trying to get the list of content blocks. We should not be able to do this, and we expect to be redirected to the log-in screen. The assert_redirected_to new_session_path uses the RESTful route to the session controller’s new action. This is where login is initiated.

Run the test, and success!

5. Writing New Unit Tests

Now that we have our existing tests working, and perhaps you’ve tried your hands at an extra functional test, let’s take a brief moment to look at the unit tests of our models. We’ll build a couple of simple tests to get you started.

a. Fixtures with Associations

First, we’ve mentioned in our lectures that fixtures have the ability to not only capture simple values representative of working and broken versions of our models, but also can represent associations such as has_many, has_one, belongs_to, and has_and_belongs_to_many (HABTM). The fixture implementation in Rails 2 allows us to set these relationships up in a natural way, specifying these associations by name.

Let’s look at the fixtures in two of our models, Links and Categories, to see this in action. These two models have HABTM associations. When you have models instantiated in code, and you have used the association methods to identify the relationships, you gain a number of useful utility methods to address the members of the relation. In much the same way, we can “name names” in fixtures.

Suppose we had this links.yml file:

	apple:
	  name: Apple
	  url: http://www.apple.com/

	google:
	  name: Google
	  url: http://www.google.com/

	learningrails:
	  name: LearningRails PodCast
	  url: http://www.learningrails.com/

and this categories.yml file:

	site:
	  name: Site
	blog:
	  name: Blog
	podcast:
		name: Podcast

To indicate which links are currently categorized, we’ll add an attribute “categories:” and literally list out the name of each category fixture member:

	apple:
	  name: Apple
	  url: http://www.apple.com/
	  categories: site

	google:
	  name: Google
	  url: http://www.google.com/
	  categories: site

	learningrails:
	  name: LearningRails PodCast
	  url: http://www.learningrails.com/
	  categories: podcast, site

Note that for “apple” and “google”, we’ve only associated one category with each entry, but we’ve still named the attribute “categories”. Rails will be able to tie the “site” category fixture entry to the associated “link” fixture when it creates the models for you during tests.

“learningrails”, on the other hand, has two categories associated with it, podcast and site, so we list these out as a comma separated list.

Let’s look at the categories.yml file:

	site:
	  name: site
	  links: apple, google, learningrails

	blog:
	  name: blog

	podcast:
	  name: Podcast
	  links: learningrails

Here we do the same thing, and link up (no pun intended) the association from the other side. We have three instances of a “site”, so we list them out as “links” (apple, google, learningrails). We have one podcast, but we still identify it with the plural form “links”. Note also that we don’t have any “blog” items, so that entry in the fixture doesn’t use the “links” identifier.

Naming conventions are important here, and the capitalization and pluralization we’ve used is critical.

Now that we have a data set set up, let’s test it out.

b. Testing Associations in our Model

We are going to load our fixtures into the CategoryTest class, just like we did for functional tests earlier:

   class CategoryTest < ActiveSupport::TestCase
      fixtures :categories, :links
	
      ...

Note that since there are relationships between the models, we need both fixture data files. If one or the other was missing, this would not work.

Now, let’s write a simple test that confirms the test data we just created (and demonstrates that Rails’ use of Test::Unit, the testing framework behind all of this magic, auto loads our data). This test will first check that our “site” category has three links using the notation we explained earlier for referring to fixture members:

   def test_category_has_links
      catsite = categories(:site)
      assert_not_nil catsite
      assert_equal 3, catsite.links.length
   end	

Here, we load a local variable “catsite” with the object defined with the “site” name in the categories.yml file. Recall we set up three links, so after asserting that we actually loaded the object with assert_not_nil, we refer to the HABTM derived method “links”, and check that three objects are in that array. Success!

We could further test that the data is also loaded into the test database by using a finder method:

   def test_category_load_site_category
      catsite = Category.find_by_name('site')
      assert_not_nil catsite
      assert_equal 3, catsite.links.length
   end

Again, we should test and have success!

c. One more unit test

Let’s do a slightly more realistic unit test. Let’s add a validation to one of our models and make sure it is working. In the Category model, let’s validate for the presence of the name attribute:

   class Category < ActiveRecord::Base
      has_and_belongs_to_many :links
      validates_presence_of :name
   end	

In our CategoryTest class, let’s add two tests that simply try to create an empty model and another that creates a model with a name. The first should fail and the second should succeed:

  def test_should_create_category_with_name
    assert_difference 'Category.count' do
      Category.create(:name => 'testname')
    end
  end
  def test_should_not_create_category_no_name
    assert_no_difference 'Category.count' do
      Category.create()
    end
  end	

The assert_difference and assert_no_difference methods run the code snippet provided as a parameter before and after their block executes (in this case, the code that tries to create a Category object). Category.count queries the database and gets the number of Category objects that currently exist. In the case where a create method works, the count should be different (one greater). In the case where it fails, the count should be identical.

We run out tests and… success? Our new tests are working, but one of our old tests is failing!

When we added the validation in the Category model, the test case test_should_create_category(CategoriesControllerTest) broke, because it tries to create an empty Category. To fix this, we can simply supply a name parameter:

  def test_should_create_category
    login_as :quentin
    assert_difference('Category.count') do
      post :create, :category => { :name => 'testname' }
    end

    assert_redirected_to category_path(assigns(:category))
  end	

Run the tests again, and finally, we have everything working again.

6. Further Exercises

  1. Install the ZenTest gem and take autotest for a spin (gem install ZenTest). This will continually run your tests as you code and save files. A real time saver.
  2. Fix the remaining (if any, purposely) broken tests in the lab6 or later source (release 6 in Unfuddle).
  3. Add a few validations to the existing models and create the associated tests to check good and bad data test cases.
  4. links_controller_test is missing a couple of tests for two actions we added later in development. What are they and can you write those tests?
  5. Fix up the email settings by moving the initializer code out of mail.rb and in to the different environment files.
  6. Add the Spider plugin for a simple Integration Test that crawls your site (script/plugin install svn://caboo.se/plugins/court3nay/spider_test).

Viewing all articles
Browse latest Browse all 27

Trending Articles