At the Forge - Testing with Rails

by Reuven M. Lerner

During the last few months, we have looked at Ruby on Rails, an open-source Web application framework written in the Ruby language meant to make Web/database development particularly easy. We already have looked at many facets of Rails development—the division of applications into model, view and controller components; subclassing ActiveRecord for automatic object/database mapping; and the use of Rails validators to ensure the integrity of data stored in the database.

Since starting to work with Rails several months ago, I have been impressed with its design and execution. Developing in Rails feels a bit different and funny at first, but you quickly can get into it, enjoying the fact that much of the tedium has been taken care of by ActiveRecord automatically.

Although Rails can reduce the amount of code we must write in our Web/database applications, it cannot reduce it to zero. And wherever there is code, there are bound to be bugs. As experienced Web/database developers know, testing these sorts of applications can be a bit tricky, because there is a disconnect between what the client sends and sees, and what actually happens with the server. It's not unusual, even today, for Web developers to debug their applications using a combination of print statements and error logs. Indeed, I am personally guilty of this on many occasions, partly out of habit and partly because this is often the best way to find problems with projects.

Managers and programmers alike know that it is cheapest and easiest to fix bugs as soon as they occur, and at an early stage of a project. But programmers often are reluctant to test their software, especially when such testing can be time consuming and tedious.

A relatively simple solution thus has emerged during the past few years, in which programmers were responsible for not only testing the software they had worked on, but also for writing the tests that would check the software in as many places as possible. Such unit testing can help ensure that each individual part of the system is robust, allowing us to depend on it when integrated into a full application.

This month, we look at the built-in Rails functionality for performing unit tests and whet our appetite for writing functional tests as well.

Test Database

Everything we have seen so far might seem reasonable, but there is a danger lurking beneath the surface. If we actually go ahead and test all of our code, we are likely to end up adding, modifying and deleting data in the actual database. On a serious production system, this could be more than inconvenient, it might cause untold problems.

If you have been following along since we first began to work with Rails, you might well remember that we defined three different databases for each of the projects we have worked with. In the case of the simple Weblog application we examined the last few months, we created three databases: blog_development, blog_test and blog_production. We completely ignored the _test and _development databases, concentrating solely on the _production version. Now that we will start testing our application, we will be using the _test database. Only when we are sure that our database and application have passed the test suite will we move it over into the production system.

If you have not already done so, create the test database and load its definitions. On my system, I created a blog user for PostgreSQL and executed the following:

$/usr/local/pgsql/bin/createdb -U blog blog_test

I then loaded the database definitions that I had saved in the blog/db directory, in a file called create.sql:


$ /usr/local/pgsql/bin/psql -U blog blog_test < blog/db/create.sql

This loads the table definitions. Assuming that create.sql was identical when I did the same with the development database, I can now assume that the development and test databases are defined in the same way.

But what if we have not been so good about updating create.sql with each modification we've made to our development database? Are we then forced to compare the two database structures manually, update create.sql and then re-import the definitions?

Luckily, the answer is no. Rails comes with a short program, clone_structure_to_test, that copies the structure of the development database to the test database. Note that it copies only the structure, not the contents. To invoke it, switch to the main application directory (blog, in our case) and use the rake, or Ruby make, program, which executes the appropriate section of the Rakefile in the current directory:

$ rake clone_structure_to_test

If the blog_test database does not yet exist, or if there are other issues, you will get an error message. Otherwise, you see only basic output, as I did:

[reuven@server blog]$ rake clone_structure_to_test
(in
/home/reuven/blog)

I encountered some initial problems with clone_structure_to_test, with the script claiming I was not the owner of the public schema in PostgreSQL. I got around this by giving the blog database user superuser permissions, which is necessary for the cloning process to work correctly:

$ /usr/local/pgsql/bin/psql blog_test

blog_development=# alter user blog createuser;
ALTER USER

Now that we have our test database in place, we can start to write some tests. But where will we put them? Rails, in its typical style, already has defined a location for the tests and assumes we will follow the same convention as the author and other Rails users. This means looking in the blog/test directory, parallel with the blog/app and blog/db directories.

The blog/test directory contains four subdirectories and a single file of Ruby code, all of which are standard in a Rails application. The four subdirectories are fixtures, functional, mocks and unit, and refer to different parts of the testing mechanism that we are expected to create or modify.

Fixtures

Before we can begin testing, however, we need to overcome a problem: if we want to test our application, we should first populate the database tables with data. Moreover, we want it to be the same, consistent data each time we run our tests, so that we can know what is being tested. Rails solves this problem with fixtures, which automatically populate our test database before we want to test. The directory blog/test/fixtures in our system is where we can create such fixtures, generally using YAML, yet another markup language, whose structure is largely determined through indentation.

We create one YAML file for each database table we want to test. Our database contains only one table, so we have to create only a single YAML file, blogs.yml. Sure enough, when we look at the blog/test/fixtures directory, we see that there is already such a file there, demonstrating how we create our fixtures and pointing us to documentation at ar.rubyonrails.org/classes/Fixtures.html, in case we don't completely understand how fixtures work.

We now create one or more entries in our blog.yml file, each of which corresponds to a single row in the Blogs database table, corresponding in turn to a single instance of the Blog object defined in our blog/app/models directory. As a reminder, our table definition is as follows:

CREATE TABLE Blogs (
id           SERIAL   NOT NULL,
title        TEXT     NOT NULL,
contents     TEXT     NOT NULL,
posted_at    TIMESTAMP      NOT NULL  DEFAULT NOW(),

 PRIMARY KEY(id)
);

Here is how we could create two fixtures for this table:

blog_entry_one:
    id:    1
    title:    My first entry!
    contents:  It was a dark and stormy night, and
I forgot my umbrella.  So I decided to tell the world
on my blog.
    posted_at: 2005-Sep-1 22:00:00

blog_entry_two:
    id: 2
    title:    My second entry!
    contents:  It was much nicer this morning.
    posted_at: 2005-Sep-1 22:00:00

This is the equivalent of two INSERT statements. Given that INSERT is standard SQL, why would we prefer to use fixtures?

To begin with, fixtures ensure that we always start with the same baseline data. It's terribly frustrating to run tests, only to have them fail because of a uniqueness constraint that was triggered by duplicate data.

The second reason is that it allows us to test our database objects not only in the database itself, but also from a source. That is, the Rails test system loads the data in our YAML file into the database, and then accesses them via our model objects. It then loads the values from our YAML file a second time, making them available via a hash. We can then compare the two, ensuring that data was imported correctly before we begin to test more sophisticated methods.

Unit Tests

With our fixture in place, we now can begin to execute the first of our unit tests. Unit tests check individual methods and objects. If an application's components pass a complete set of unit tests, there is still room for error. However, those errors tend to be from the integration of the units, which are covered by a different set of tests.

Not surprisingly, unit tests are defined inside of the test/unit directory within our main application directory. Rails automatically creates a unit test file for each model class you have defined; thus, my test/unit directory contains a file named blog_test.rb, corresponding to the Blog class defined in app/models/blog.rb. (Remember: Rails model objects have singular names and refer to database tables that have plural names.)

By default, the file contains a skeleton for our unit tests:


require File.dirname(__FILE__) + '/../test_helper'

class BlogTest < Test::Unit::TestCase
  fixtures :blogs

  def setup
@blog = Blog.find(1)
  end

  # Replace this with your real tests.
  def test_truth
assert_kind_of Blog,  @blog
  end
end

The first line helps to bootstrap the test mechanism and is not of immediate interest. But then we see that we are defining a class (called BlogTest, using the Rails naming convention that we would expect in a file called blog_test.rb). BlogTest is a subclass of Test::Unit::TestCase, which comes with Rails, and provides us with a number of different test-related features.

The definition of BlogTest begins with a declaration, fixtures, whose value is :blogs—an indication that when Rails wants to test our Blog object with the BlogTest object, it should first populate the test database with fixtures defined in test/fixtures/blogs.yml. If we are interested in using multiple fixtures, we can name them as well:

fixtures :blogs, :foo, :bar

Two methods are defined for us by default, called setup and test_truth. The setup method, as you might expect from its name, gets things ready for our tests. In this particular case, it invokes Blog.find(), giving it a parameter of 1. In other words, it retrieves the object whose primary key is 1 (that is, blog_entry_one) and puts it into @blog. Perl programmers might need to remember that in Ruby, @blog is an instance variable, not necessarily an array. In this particular case, @blog contains a single instance of Blog—the object that Blog.find(1) returned.

The other method, test_truth, then uses one of the predefined test assertions that comes with Rails. In this particular case, we use the assert_kind_of assertion to check that @blog is an instance of Blog.

To run this initial version of our tests, we simply say:

$ ruby blog_test.rb

As the tests run, we get a status report. If all goes well, the output should look like this:

[reuven@server unit]$ ruby blog_test.rb
Loaded suite blog_test
Started
.
Finished in 19.066227 seconds.

1 tests, 1 assertions, 0 failures, 0 errors

The first few times that I ran these tests, I received error messages. The first problem was that I had failed to change blogs.yml from its original, default state, without defining anything but the id primary key field. Because of the integrity constraints that I had put on the Blogs table, PostgreSQL stopped the test, indicating that it would not allow NULL values in the title field.

The second time I tried to run the tests, Rails picked up an error in my YAML file, reminding me that the YAML format requires consistent indentation with spaces and without any tab characters.

Rails wisely distinguishes between failures and errors; both of the above were classified as errors, letting me focus on the overall test environment rather than a particular method.

We can add additional tests by defining new methods. For example, let's check that the title of @blog matches the value we put in the YAML file. We can add the following to the BlogTest class definition:

def test_title
assert_equal @blogs["blog_entry_one"]["title"], @blog.title
end

Notice how our test begins with the characters test_. This tells Rails that this method should be part of our test suite. Because each individual method is counted separately, it is probably best to have a large number of test methods, each of which contains a small number of assertions. There is no technical reason why you cannot put a large number of assertions in the same method, but it means you might have a tougher time understanding just where the problem lies.

In this case, we are using assert_equal to check that two quantities are equal. Pay close attention to the very similar names, and you will see what we are doing. The first item to test for equality is @blogs[“blog_entry_one”][“title”]. The @blogs hash is created automatically for us by the test suite, and (as mentioned earlier) contains the entire YAML fixture definition file. If @blogs contains the entire YAML definition, then @blogs[“blog_entry_one”] contains the first fixture, and @blogs[“blog_entry_one”][“title”] contains the title of the first one.

@blog, by contrast, has a singular name because it contains only a single instance of Blog. And like all good objects descended from ActiveRecord, we can use a method to retrieve the contents of a field—in this case, @blog.title. So, the bottom line is that this test helps us check that the title is the same.

More Testing

The above are only two of the types of tests you might want to use on your system. Rails comes with a large collection of assertions, allowing you to test your models in a great variety of ways.

Remember that methods are just one part of the testing equation; you also will want to have appropriate integrity constraints and checks in your table definitions, and a wide variety of inputs to ensure that you are checking many different possibilities. One way to create a large number of fixtures is by creating them dynamically, using the same syntax (known as ERb, or Embedded Ruby) that is used in Rails views.

As I mentioned above, functional tests are another important element in any application's test suite. Functional tests, which operate against Rail controllers, work similarly to our unit tests—in the tests/functional directory, with one test object per controller, and with a test_ method for each method in the controller object. Testing models ensures that your data is going to be robust; testing controllers ensures that no matter what inputs you receive from users via the Web, the application will handle the situation gracefully.

Finally, Rails makes it easy to create mock objects, allowing us easily to pretend that an object has been created. For example, we might want to pretend that a credit-card transaction has gone through, or that we have sent e-mail to 50,000 users of our system, without actually carrying out the task.

Conclusion

Web applications are becoming large and sophisticated enough that they demand disciplined testing techniques to avoid unforeseen problems. Ruby on Rails comes with an integrated test system that makes it easy to create and use tests at all levels—database, model objects and controller objects. It shouldn't come as any surprise that many Ruby developers are fans of test-driven development, in part because Ruby and the Rails environment make it so easy to accomplish. If you are going to develop with Rails, it's worth taking the extra time to add tests into your application. It's easy to do, and it will save you a great deal of time later on.

Resources for this article: /article/8631.

Reuven M. Lerner, a longtime Web/database consultant and developer, now is a graduate student in the Learning Sciences program at Northwestern University. His Weblog is at altneuland.lerner.co.il, and you can reach him at reuven@lerner.co.il.

Load Disqus comments