Testing in Rails: Part 5 - Unit Testing ActiveRecord Models

This is part of an ongoing series of posts about how to get started writing tests for Ruby on Rails. The series begins with the introduction and overview of the ideas behind testing.

Setting Up for Unit Testing ActiveRecord Models

Wine

In the previous section, we saw that running unit tests inside the Rails framework is not that different from running tests outside it. We have learned how to test the Car class in both Ruby and Rails. But Car does not inherit from ActiveRecord::Base (or, if yours did, our tests did not probe any ActiveRecord traits). Let’s see how our unit tests would be different if our class was using ActiveRecord to store instances in a database.

For this example, we will keep using the same sample application, but we will move away from cars and create two new classes: Winery and Wine. Each winery will produce several different wines. We start by using script/generate to create our two models.

script/generate model Winery
script/generate model Wine

The generator will create the model, a migration, a skeleton for your unit test and even a fixture file we will use later, all in one easy step. You do not need to worry about creating controllers, scaffolding or views. They are irrelevant for unit testing.

We will keep the tables simple. Here are my two migration files:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CreateWineries < ActiveRecord::Migration
  def self.up
    create_table :wineries do |t|
      t.column :name, :string
      t.column :city, :string
      t.column :state, :string
      t.column :country, :string
      t.column :created_at, :datetime
      t.column :updated_at, :datetime
    end
  end

  def self.down
    drop_table :wineries
  end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CreateWines < ActiveRecord::Migration
  def self.up
    create_table :wines do |t|
      t.column :name, :string
      t.column :year, :integer
      t.column :family, :string
      t.column :winery_id, :integer, :null => false, :default => 0
      t.column :created_at, :datetime
      t.column :updated_at, :datetime
    end
    add_index :wines, :winery_id
  end

  def self.down
    drop_table :wines
  end
end

If you never created a development database for sample_app (I had not), then you should do that now and provide the connection information in config/database.yml. Then run your migrations.

1
rake db:migrate

Notice that, when you migrate, it adds the tables to the development database but not to your test database. If we are going to test these ActiveRecord models in our test environment and want them to behave exactly like they do in our development and production environment, then it makes sense that we have to get the same schema into our test database. There are many ways to accomplish it and we will examine a few.

Rake Database Tasks

The first way to get the database schema into our test database is simply to perform another migration.

1
rake db:migrate RAILS_ENV=test

You have probably done something similar when migrating your development and production databases. Of course this technique depends on you having good migration files. Hopefully you do, but I also realize some people have not started using migrations yet.

Rake also provides some tasks that are specifically designed for working with your test database. You may find these Rake tasks easier in some cases, and they are essential if you are not using migrations.

Rake Command Description Same as…
rake db:test:purge “Empty the test database”  
 
 
rake db:test:clone “Recreate the test database from the current environment’s database schema” rake db:schema:dump
rake db:test:purge
rake db:schema:load
rake db:test:clone_structure “Recreate the test databases from the development structure” rake db:structure:dump
rake db:test:purge
execute the SQL commands
rake db:test:prepare “Prepare the test database and load the schema” rake db:test:clone
or
rake db:test:clone_structure

The first task simply purges the test database. That is, it drops every table in the database but leaves the database itself.

The next three tasks will create the test databases that we need. All three will give you the same results, but I want to explain the differences. The db:test:clone task exports the schema for the current environment (development) into db/schema.rb, purges the test database, then loads the schema file into the test database. The db:test:clone_structure task does something similar but it exports the database structure as SQL into db/development_structure.sql, not as a Ruby Migration, then purges the test database and loads in the SQL from the file that it exported. If you have configured ActiveRecord::Base.schema_format to be :sql, then this is probably a better choice than db:test:clone. Finally, db:test:prepare is a wrapper for the previous two tasks. It uses the value of ActiveRecord::Base.schema_format (which is :ruby by default) to choose which of the first two tasks to call. If the value is :ruby, then it will run db:test:clone. If value is :sql, then it will run db:test:clone_structure.

Try out all three. There is no harm in it. Use the db:test:purge task if you want to drop all the tables before trying another task. I think rake db:test:prepare is the best choice simply because then you never have to remember what your schema_format is. Note that all of these techniques do not use your migration files—in fact, you do not even have to have migrations—they get schema information directly from the database for the current environment.

Building Out the Models

Now that we have a database that our ActiveRecord models can use during testing, let’s work on our models. They will be simple, but still look like models we might actually use.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Winery < ActiveRecord::Base
 
  has_many :wines, :order => 'name ASC, year ASC'
 
  validates_presence_of :name
  validates_uniqueness_of :name
  validates_length_of :name, :maximum => 255
  validates_length_of :city, :maximum => 255
  validates_length_of :state, :maximum => 255
  validates_length_of :country, :maximum => 255
 
  def location
    return [city, state, country].compact.join(", ")
  end
 
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Wine < ActiveRecord::Base

  belongs_to :winery
 
  validates_presence_of :name, :year, :family
  validates_length_of :name, :maximum => 100
  validates_format_of :year, :with => /\d{4}/,
      :message => 'must be a four-digit number'
  validates_length_of :family, :maximum => 255

  FAMILIES = [
    "Cabernet Sauvignon",
    "Merlot",
    "Pinot Noir",
    "Chardonnay",
    "Sauvignon Blanc"
  ].freeze
 
  def self.current_families
    all_wines = self.find(:all, :order => 'family ASC')
    return all_wines.collect {|w| w.family}.uniq!
  end
 
  def self.find_newest
    self.find(:first, :conditions => 'year IS NOT NULL', :order => 'year DESC')
  end
 
  def self.find_oldest
    self.find(:first, :conditions => 'year IS NOT NULL', :order => 'year ASC')
  end
 
  def listing_name(brief=true)
    if brief
      return "#{name}, #{family} (#{year})"
    else
      output = "#{name}, a #{family} from #{year}, by #{self.winery.name}"
      output << " in #{self.winery.location}." if self.winery.location != ""
      return output
    end
  end

  def age
    Time.now.year - self.year
  end
 
  def is_antique?
    self.age >= 75 ? true : false
  end
 
end

WineTest Setup

Before we can start testing, we will need some sample data to use. This is similar to what we did with the Car class earlier. The only difference is that we will save our instances to the database. (Strictly speaking, you don’t have to save them but you generally will.) Let’s create the setup method for WineTest (test/unit/wine_test.rb).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
require File.dirname(__FILE__) + '/../test_helper'

class WineTest < Test::Unit::TestCase
  #fixtures :wines

  def setup
    @sunnyside = Winery.create(:name => "Sunnyside Vineyards",
      :city => "Sonoma", :state => "CA", :country => "USA")
    @merlot = Wine.create(:name => "Sunnyside Reserve",
      :year => 2003, :family => "Merlot")
    @pinot_noir = Wine.create(:name => "Valley Creek",
      :year => 1998, :family => "Pinot Noir")
    @sunnyside.wines = [@merlot, @pinot_noir]
  end
 
end

Note that I commented out the fixtures line again. We will get to fixtures soon though. ;-)

In the setup method I created a winery and two wines using sample data. I called create instead of new so that the instances will be saved to the database right away. Now we have some database data to use in our tests. Remember, you can use any Ruby you want to get the objects and the database tables into exactly the state you need. Use Ruby to set the stage.

The Pitfall in Creating Test Records

Before we start writing our tests, I need to show you a potential pitfall that comes when you create records in your tests. Especially if you create them in the setup method. Try adding this simple test to WineTest.

1
2
3
4
5
6
7
8
class WineTest < Test::Unit::TestCase
  ...

  def test_id
    assert_equal(1, @merlot.winery_id)
  end

end

Now run your test.

1
2
3
4
5
6
7
>ruby test/unit/wine_test.rb
Loaded suite test/unit/wine_test
Started
.
Finished in 0.053157 seconds.

1 tests, 1 assertions, 0 failures, 0 errors

It worked. Big deal. But now try running the test a second time.

1
2
3
4
5
6
7
8
9
10
11
12
>ruby test/unit/wine_test.rb
Loaded suite test/unit/wine_test
Started
F
Finished in 0.061369 seconds.

  1) Failure:
test_id(WineTest) [test/unit/wine_test.rb:15]:
<1> expected but was
<2>.

1 tests, 1 assertions, 1 failures, 0 errors

What happened? We did not change any code. Why did the first succeed and the second fail? It reports that the problem was that the winery_id was 2 instead of 1. We might guess that the database records created in our first test were left behind (i.e. a winery with ID 1) which would cause a second winery to be given an ID of 2. But if we look in the test database, we will find that the wines and wineries tables are empty.

How the Test Database is Managed

Two things are happening to give us this strange behavior and they are important to understand.

1) The tests are performed as transactions, meaning that the database is being rolled back to its original state after the test is complete. If we create a record in setup that record will be removed at the end. That explains why the tables in the test database are empty.

2) The id field of both tables is set to ‘auto_increment’ and the database keeps track of the next number to use as an index. When the records created in the first test are deleted, the AUTO_INCREMENT value is not changed. It continues to count upward after each test’s setup method is called.

A simple rake db:test:prepare before running the tests would drop all of the tables in the test database and recreate them, thereby also setting AUTO_INCREMENT back to 0. That would make our relationship test run successfully (try it and see), but it would not keep AUTO_INCREMENT from increasing with each test in WineTest.

To see what I mean, try adding another simple test method to WineTest. It is important that the test is called “test_first” so that it executes before “test_id”.

1
2
3
4
5
6
7
8
class WineTest < Test::Unit::TestCase
  ...

  def test_first
    assert true
  end

end

Now run your tests again making sure to prepare the test database first.

1
2
3
4
5
6
7
8
9
10
11
12
13
>rake db:test:prepare
>ruby test/unit/wine_test.rb
Loaded suite test/unit/wine_test
Started
.F
Finished in 0.096135 seconds.

  1) Failure:
test_id(WineTest) [test/unit/wine_test.rb:19]:
<1> expected but was
<2>.

2 tests, 2 assertions, 1 failures, 0 errors

Setup was run twice, once before each test. Both times it created a new winery and the value of AUTO_INCREMENT increased even though it deleted the records at the end of each test. rake db:test:prepare got our database into shape before we started but it could not help once the tests were running.

Remember: when you add records to the database, the value of AUTO_INCREMENT will not be reset and may become difficult to predict.

I realize that seems kinda sucky. You could write your tests to take into account the non-resetting behavior of AUTO_INCREMENT. The downside would be that adding a test might throw the IDs in all of your other tests off.

But I think there is another solution that will make it suck less. But, even with this solution, you will still need to keep this fact about auto-incrementing in mind anytime you save new records to the database.

What can help to ease our setup pain? Fixtures.

(I told you we would get to fixtures soon enough…)

UPDATE: The series continues in Part 6.

Bookmark and Share

6 Responses to “Testing in Rails: Part 5 - Unit Testing ActiveRecord Models”

  1. wear Says:

    Pls go on this series, I need it, and I think many reader here need it two ! :)

  2. Kevin Skoglund Says:

    More coming real soon… I promise!

    End of the year client work and holiday shopping have taken up the spare time I usually use to write these.

  3. A Fresh Cup » Blog Archive » Double Shot #100 Says:

    [...] Testing in Rails: Part 5 - Unit Testing ActiveRecord Models - Part of the ongoing series from Null is Love. [...]

  4. Richard OnRails Says:

    Hi Kevin,

    Wow, that auto-increment and unit testing interaction is tricky. I haven’t saved any data via unit-test yet, so your very lucid explanation was a great eye-opener. Thanks for taking the trouble to announce the problem and illuminate the solution all at once.

    Best wishes,
    Richard

  5. daniela Says:

    Thanks! I’ve been meaning to learn about testing for ages. This is really helpful, working myself through it. the id issue with autoincrementing is particularly useful, looking forward to the callbacks testing!
    Daniela

  6. sk Says:

    Hi
    I am using rails 2.3.2 But the autoincrementing issue is not there. Am I right?
    sk

Leave a Reply