Unit testing in Ruby

This article was originally published in VSJ, which is now part of Developer Fusion.

Over the last decade or so developers have recognised how important testing in general and unit testing in particular have become. There are unit testing frameworks for all the major languages and in some cases more than one framework. Ruby is no exception. In fact, as Ruby is a dynamic language unit testing is even more important. In static languages the compiler will find many errors. With dynamically typed languages such as Ruby there is no compiler around to check the code, so it is even more important that unit tests are in place to help verify the code’s correctness.

Testing in Ruby?

Ruby offers a couple of test frameworks, among these are Test::Unit which is available in Ruby 1.8 and MiniTest::Unit available in Ruby 1.9. These are very similar, and tests written with Test::Unit will mostly work with MiniTest::Unit. The primary differences are that MiniTest drops the GUI tools that Test::Unit contained and MiniTest::Unit has replaced Test::Unit’s assert_not functions with corresponding refute functions. I’m going to use Test::Unit as most people are still using Ruby 1.8.

To illustrate some of the concepts we should first have something to test. In respect of all the many articles and training classes on testing we’ll use a calculator as the first example. Our initial calculator class will be simple:

class Calc
    def add
    end
end

…and we can now set about testing it.

Our test class has to use the Test::Unit classes and so should require them. The test class also needs to derive from TestCase, this will allow Test::Unit to bring in the bits it needs to run the tests.

Initially we’ll place the test code in the same directory as the code under test (we’ll change this in a few minutes), this means our test code will look like this:

require 'test/unit'
require 'calc'
class TestAdd < Test::Unit::TestCase
    def test_add
    	calc = Calc.new
    	expected = calc.add 3,2
    	assert_equal expected, 5
    end
end

There are several things to note here. As I mentioned above we require ‘test/unit’ to bring in the testing code and we then derive from TestCase, this gives us access to the asserts and other helpers in Test::Unit. The name of the test method starts with ‘test’, this is how the test framework identifies this as being a test. In here we create an instance of a calculator and call its add method, all standard Ruby. Then we use one of the Unit::Test helpers, assert_equal, this does exactly what it says on the tin and checks the equality of the two values. We can run this from a shell:

$ ruby test_add.rb
Loaded suite test_add
Started
F
Finished in 0.01038 seconds.

    1) Failure:
test_add(TestAdd) [test_add.rb:10]:
<nil> expected but was
<5>.

1 tests, 1 assertions, 1 failures, 0
    errors

…and of course the test fails (remember our calculator currently does nothing), so we fix the calculator and run the test again:

Loaded suite test_add
Started
.
Finished in 0.000319 seconds.

1 tests, 1 assertions, 0 failures, 0
    errors

That’s much better. Notice that in keeping with other test frameworks in other languages the output of the tests are simple, a ‘.’ for each test that passes meaning we can scan the output quickly and see that everything passes (or something fails) very easily. We can add a second test to see this, something like:

def test_add_bigint
    calc = Calc.new
    val = calc.add 1000000000,1000000000
     assert_equal val, 2000000000
end

Loaded suite test_add
Started
..
Finished in 0.000407 seconds.

2 tests, 2 assertions, 0 failures, 0
    errors

Note the two dots, so we have an easy-to-read result.

We have an issue at the moment with the way the code is structured in terms of the layout on disk. The tests live in the same directory as the code under test. While this works, as we see above, it’s far from ideal. How do we know which is the test code and which is the system code? When we come to ship we probably do not want to ship the test code along with the system code. Because of this it’s good to separate the code into different directories. The simplest approach is to create two directories – one for the application (app) and one for the tests (test), so we end up with a structure that looks like this:

root
|
-- app
|
-- test

…where ‘root’ is some directory for this application, app contains the application code and test the testing code. When we do this we have to amend the test code so that it requires the app directory:

require 'test/unit'
require 'app/calc'

The rest of the code stays the same.

To run the code we start a shell in the ‘root’ directory and tell Ruby where to find the test code, so something like:

$ ruby -I . test/test_add.rb

The -I . tells Ruby to include the current directory in it’s search path, if everything is OK we’ll see the same results as before, but now we have a good separation of the code from the tests.

Rails Testing

How does this fit with Rails? If you’ve used Rails or seen my previous articles you’ll know that a standard Rails application has a directory structure which looks something like:

root
|
-- app
|
-- controllers
|
-- ...
|
---- other stuff
|
-- test

…which is similar to what we have above. The test directory is further sub-divided into ‘unit’, ‘functional’ and ‘integration’ sub directories. These contain tests for different areas of your application. There is also a ‘fixtures’ sub directory that we’ll come back to. The unit directory will contain our unit tests, these will be the tests for the model classes in the Rails application. Functional tests will typically test our controllers and integration testing involves end-to-end testing.

When you create a model in Rails a unit test is created automatically. If the model is called user there will be a test file called user_test.rb, this will contain a class with one method:

def test_truth
assert true
end

…which seems bizarre, assert that true is true!

The reason for this is to check that the test environment is configured correctly. You can run this test from the shell:

$ ruby -I test test/unit/user_test.rb

If you do this the test will fail with extreme prejudice! The reason being that the test environment has not been set up correctly. To run the model tests we must first have a test database, we’ve been using MySql so we fire up a MySql prompt and run (if this is our blog software):

create database blog_test;

But we’re still not quite there, we also have to migrate the current schema to the test database, from a shell run:

rake db:test:prepare

Once completed we can now run the test again, and this time it should succeed, true really is true!

You can also run a rake task to execute the unit tests:

rake test:units

This will execute the prepare task for you so if you use only this task there’s no need to manually initialise the test database. These tests work the same way as any other unit test, we will see a ‘.’ for a successful test and a failure message otherwise.

As an example our blog class looks like this:

class Blog < ActiveRecord::Base
    validates_presence_of :title
    validates_presence_of :sub_title
    validates_presence_of :nickname
    validates_uniqueness_of :nickname
    validates_presence_of :owner_id
    belongs_to :user, :foreign_key
    	=> "owner_id"
    has_many :blog_entries
...
end

…and we should test that validation code, so a test may look like:

class BlogTest < ActiveSupport::TestCase
    def test_blog_has_valid_fields
    	blog = Blog.new
    	assert !blog.valid?
    end
...
end

With unit tests we are typically testing models, and these models are backed by a database. For certain tests we would like the test database to contain specific data we could use. The way that is done in Rails is through the use of fixtures.

A fixture is a file, typically YAML but it could be CSV, that contains data for a table. A fixtures file may look like this:

blog_with_nickname:
    title: MyString
    sub_title: MyString
    nickname: kevin
    owner_id: 1

blog_with_no_nickname:
    title: MyString
    sub_title: MyString
    owner_id: 2

The values ‘blogwithnickname’ and ‘blogwithno_nickname’ are the names of the fixtures. These names could be anything and in fact should be meaningful as we will use them in the test code. The name of the file containing the fixture must match the name of the database table, in this case ‘blogs’ and the formatting of the data in the fixture is significant. By default rails will load the fixtures associated with a unit test (convention over configuration again), but if necessary you can load specific fixtures as part of a test by including the line:

fixtures :blog

…at the top of the class containing the unit tests.

The data in the fixtures are loaded into the database before each test method is run, so each test starts with a clean database.

Within a test you reference a specific fixture by name, so within a test we could say something like:

blog = blogs(:blog_with_nickname)

…where blogs is the name of the file containing the fixture, this takes the name of the fixture as its argument. This will load the fixture from the database. We could have a simple test, something like this:

def test_valid_load
    blog = blogs(:blog_with_nickname)
    assert blog.valid?
end

Fixtures are extremely flexible, for example you can also run Ruby code to create a fixture. Maybe you want to insert a number of rows into a table that are all very similar, we could write something like this:

<% 0.upto(9) do |i| %>
blog_fixture_<%= i %>:
nickname: nickname<%= i %>
title: title<%= i %>
<% end %>

…to load ten entries.

Conclusion

Ruby like other programming languages provides support for unit testing. This is more important in dynamic languages as you have no compiler around to help check type correctness. Rails builds on Ruby’s test support and offers several useful features. Rails tests are separated into unit, integration and functional tests, use these as they are meant to be used, i.e. keep your unit tests short! Rails also provides the concept of test fixtures which help with testing models as they let us pre-load database tables for each test.

You might also like...

Comments

About the author

Kevin Jones United Kingdom

Kevin Jones has been involved in software development and design for more years than he cares to remember, and has been involved in training and consultancy since 1990. He is the co-author of Se...

Interested in writing for us? Find out more.

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“If debugging is the process of removing software bugs, then programming must be the process of putting them in.” - Edsger Dijkstra