Testing in Rails: Part 4 - Unit Testing in Rails

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.

Unit Tests + Rails

After so much discussion of unit testing in Ruby you are probably anxious to start applying your new-found skills to a Rails application. The good news is that everything you have learned so far is directly applicable to testing in Rails.

When you first create a new Rails application (e.g. rails sample_app), a folder called ‘test’ is created in your application’s root directory. This is the folder where all of your test code will reside.

Test Folder

As you would expect, we will be putting all of our application’s unit tests inside the subfolder called ‘unit’. At first that folder will be empty. When a new model is generated, the generator script will also create a skeleton for unit testing that model.

Let’s stick with our Car model as an example.

1
2
3
4
5
6
7
8
9
10
11
12
>rails sample_app
... (application files get created)
>cd sample_app
>script/generate model Car
    exists  app/models/
    exists  test/unit/
    exists  test/fixtures/
    create  app/models/car.rb
    create  test/unit/car_test.rb
    create  test/fixtures/cars.yml
    create  db/migrate
    create  db/migrate/001_create_cars.rb

Notice that it created ‘test/unit/car_test.rb’ and ‘test/fixtures/cars.yml’. We will learn to use fixtures later. For now, all you need to know is that fixtures will provide a way to load sample data into a database that your tests can use.

Open up cart_test.rb.

1
2
3
4
5
6
7
8
9
10
require File.dirname(__FILE__) + '/../test_helper'

class CarTest < Test::Unit::TestCase
  fixtures :cars

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

Notice that this test does not load in Test::Unit as we did at the start of our Ruby tests (i.e. require ‘test/unit’). The Rails framework has already made sure Test::Unit is ready and waiting for us.

It does however load in ‘../test_helper’ which can be found in the root of the ‘test’ directory. This file will be a place to store methods that you want to be accessible to all of your applicaiton’s unit tests. It works a lot like helper methods in your Rails application. If you examine the file you will see that it adds methods to Test::Unit::TestCase which will be the superclass to all of your unit test classes.

The CarTest class should look pretty standard except for fixtures :cars. Comment that line out for now. We will come back to it later on.

One minor point: unlike much of Rails, your test class name and the file name that contains the class are not important. They do not have to be the same as your model name or have any relationship to each other. The file and class names the generator provides are useful because they clearly indicate that this test is related to your Car model, but Rails does not require you to adhere to those names.

Running Unit Tests in Rails

We can try running the tests in the same way that we ran our Ruby unit tests, by simply executing the test file with Ruby.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>ruby test/unit/car_test.rb
Loaded suite test/unit/car_test
Started
EE
Finished in 0.027768 seconds.

  1) Error:
test_truth(CarTest):
Mysql::Error: Access denied for user 'root'@'localhost' (using password: NO)
...
    /activerecord-1.15.5/lib/active_record/fixtures.rb:538:in `setup`

  2) Error:
test_truth(CarTest):
Mysql::Error: Access denied for user 'root'@'localhost' (using password: NO)
...
    /activerecord-1.15.5/lib/active_record/fixtures.rb:561:in `teardown`

1 tests, 0 assertions, 0 failures, 2 errors

The unit test executed successfully but it generated two errors. (I snipped out the unimportant bits.) The errors are telling us that our tests could not connect to a database. It does not matter that we commented out the fixtures line. Rails still tries to connect to our test database, once during setup and once during teardown. (’teardown’ is a Test::Unit::TestCase method that works like ’setup’ but is called after the test is complete.)

Test Database

This highlights the first big difference between Rails testing and Ruby testing: we need a database. Since our tests will need to be able to test creating, updating and deleting records, we absolutely will not want to use our production database.

It is not a great idea to use our development database either. We need to be able to develop and test simultaneously. If we were developing with a set of sample records in our development database we might not want our tests to wipe out that data. Instead we should create a third database.

You may already be in the habit of creating and configuring three databases each time you start a project. If you are following along with my examples, go ahead and create a database called ’sample_app_test’ and edit the ‘test’ configuration in ‘config/database.yml’ to connect to it. You do not need to create or configure the development and production databases. The Rails testing framework will only use the database specified under ‘test’.

Try running the unit tests again and this time they should pass.

The Final Step

Take all the code we previously wrote for our Car class and paste it into ‘app/models/car.rb’. (The Car model could still inherit from ActiveRecord; the unit tests we are using will not use ActiveRecord to do anything. I went ahead and made Car a stand-alone class.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# app/models/car.rb
class Car
  attr_accessor :model, :year, :color
  attr_reader :wheels
  attr_writer :doors
 
  def initialize(options={})
    self.model = options[:model] || 'Volvo'
    self.year = (options[:year] || 2007).to_i
    self.color = options[:color] || 'unknown'
    @wheels = 4
  end
 
  def self.colors
    ['blue', 'black', 'red', 'green']
  end
 
  def full_name
    "#{self.year.to_s} #{self.model} (#{self.color})"
  end
end

Take all the code from our first CarTest class and paste it into ‘test/unit/car_test.rb’.

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# test/unit/car_test.rb
require File.dirname(__FILE__) + '/../test_helper'

class CarTest < Test::Unit::TestCase
  #fixtures :cars

  def setup
    @volvo = Car.new(:color => 'green')
    @honda = Car.new(:model => 'Honda', :year => 2004, :color => 'blue')
    @dodge = Car.new(:model => 'Dodge', :year => 2003, :color => 'yellow')
  end

  def test_attributes
    new_car = Car.new
    # write to the attributes
    new_car.model = 'Test'
    new_car.year = 25
    new_car.color = 'test color'
    # read from the attributes
    assert_equal('Test', new_car.model)
    assert_equal(25, new_car.year)
    assert_equal('test color', new_car.color)
    assert_equal(4, new_car.wheels)
   
    #test the attr_reader and attr_writer
    # :wheels can be read but not written
    assert_nothing_raised(NoMethodError) { new_car.wheels }
    assert_raise(NoMethodError) { new_car.wheels = 6 }

    # :doors can be written but not read
    assert_nothing_raised() { new_car.doors = 2 }
    assert_raise(NoMethodError) { new_car.doors }
   
  end
 
  def test_initialize
    # try creating a new object without the optional options hash
    default_car = Car.new
    # test that default values were used
    assert_equal('Volvo', default_car.model)
    assert_equal(2007, default_car.year)
    assert_equal('unknown', default_car.color)

    # let's try again but with values in an option hash
    custom_car = Car.new(:model => 'Toyota', :year => 2005, :color => 'white')
    # test that options values were used instead of defaults
    assert_equal('Toyota', custom_car.model)
    assert_equal(2005, custom_car.year)
    assert_equal('white', custom_car.color)
   
    # leaving out :year; sending :color and :model out of order
    shuffled_car = Car.new(:color => 'red', :model => 'Kia')
    assert_equal('Kia', shuffled_car.model)
    assert_equal(2007, shuffled_car.year)
    assert_equal('red', shuffled_car.color)
   
    # testing :year as a string
    string_car = Car.new(:year => '2000')
    assert_equal(2000, string_car.year)
  end
 
  def test_colors
    assert(Car.colors.kind_of?(Array))
    assert_equal(4, Car.colors.length)
    assert_equal(['blue', 'black', 'red', 'green'], Car.colors)
    assert(Car.colors.include?(@volvo.color))
    assert(Car.colors.include?(@honda.color))
    assert(!Car.colors.include?(@dodge.color))
    assert(!Car.colors.include?(Car.new.color))
  end
 
  def test_full_name
    assert_equal('2007 Volvo (green)', @volvo.full_name)
    @volvo.model = 'Nissan'
    @volvo.year = 2006
    @volvo.color = 'black'
    assert_equal('2006 Nissan (black)', @volvo.full_name)

    @volvo.model = 'foo'
    @volvo.year = 1
    @volvo.color = 'bar'
    assert_equal('1 foo (bar)', @volvo.full_name)
  end
end

Run your unit tests again. You should get “4 tests, 28 assertions, 0 failures, 0 errors”.

It is that easy! Of course, this is still just testing Ruby code by dropping it inside the Rails framework. But we are using the Rails framework and our application could legitimately have a model that does not inherit from ActiveRecord but still needs unit testing (a shopping cart, a widget or a menu bar come to mind as possibilities). Using the same Ruby code illustrates how similar unit testing is in Ruby and in Rails.

Next we will examine unit testing on a more typical Rails model, one that uses ActiveRecord and a database.

UPDATE: The series continues in Part 5.

Bookmark and Share

One Response to “Testing in Rails: Part 4 - Unit Testing in Rails”

  1. A Fresh Cup » Blog Archive » Double Shot #94 Says:

    [...] Testing in Rails: Part 4 - Unit Testing in Rails - Next part of an ongoing beginners’ series from Null is Love. [...]

Leave a Reply