Rails unit test

Unit Testing your Models

In Rails, unit tests are what you write to test your models.
For this guide we will be using Rails scaffolding. It will create the model, a migration, controller and views for the new resource in a single operation. It will also create a full test suite following Rails best practices. I will be using examples from this generated code and will be supplementing it with additional examples where necessary.
For more information on Rails scaffolding, refer to Getting Started with Rails
When you use rails generate scaffold, for a resource among other things it creates a test stub in the test/models folder:
$ rails generate scaffold post title:string body:text
...
create  app/models/post.rb
create  test/models/post_test.rb
create  test/fixtures/posts.yml
...
The default test stub in test/models/post_test.rb looks like this:
require 'test_helper'
class PostTest < ActiveSupport::TestCase
  # test "the truth" do
  #   assert true
  # end
end
A line by line examination of this file will help get you oriented to Rails testing code and terminology.
require 'test_helper'
As you know by now, test_helper.rb specifies the default configuration to run our tests. This is included with all the tests, so any methods added to this file are available to all your tests.
class PostTest < ActiveSupport::TestCase
The PostTest class defines a test case because it inherits from ActiveSupport::TestCase. PostTest thus has all the methods available from ActiveSupport::TestCase. You'll see those methods a little later in this guide.
Any method defined within a class inherited from MiniTest::Unit::TestCase (which is the superclass of ActiveSupport::TestCase) that begins with test (case sensitive) is simply called a test. So, test_password, test_valid_password and testValidPassword all are legal test names and are run automatically when the test case is run.
Rails adds a test method that takes a test name and a block. It generates a normal MiniTest::Unit test with method names prefixed with test_. So,
test "the truth" do
  assert true
end
acts as if you had written
def test_the_truth
  assert true
end
only the test macro allows a more readable test name. You can still use regular method definitions though.
The method name is generated by replacing spaces with underscores. The result does not need to be a valid Ruby identifier though, the name may contain punctuation characters etc. That's because in Ruby technically any string may be a method name. Odd ones need define_method and send calls, but formally there's no restriction.
assert true
This line of code is called an assertion. An assertion is a line of code that evaluates an object (or expression) for expected results. For example, an assertion can check:
  • does this value = that value?
  • is this object nil?
  • does this line of code throw an exception?
  • is the user's password greater than 5 characters?
Every test contains one or more assertions. Only when all the assertions are successful will the test pass.

3.1 Preparing your Application for Testing

Before you can run your tests, you need to ensure that the test database structure is current. For this you can use the following rake commands:
$ rake db:migrate
...
$ rake db:test:load
The rake db:migrate above runs any pending migrations on the development environment and updates db/schema.rb. The rake db:test:load recreates the test database from the current db/schema.rb. On subsequent attempts, it is a good idea to first run db:test:prepare, as it first checks for pending migrations and warns you appropriately.
db:test:prepare will fail with an error if db/schema.rb doesn't exist.
3.1.1 Rake Tasks for Preparing your Application for Testing
Tasks Description
rake db:test:clone Recreate the test database from the current environment's database schema
rake db:test:clone_structure Recreate the test database from the development structure
rake db:test:load Recreate the test database from the current schema.rb
rake db:test:prepare Check for pending migrations and load the test schema
rake db:test:purge Empty the test database.
You can see all these rake tasks and their descriptions by running rake --tasks --describe

3.2 Running Tests

Running a test is as simple as invoking the file containing the test cases through rake test command.
$ rake test test/models/post_test.rb
.
Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s.
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
You can also run a particular test method from the test case by running the test and providing the test method name.
$ rake test test/models/post_test.rb test_the_truth
.
Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s.
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
This will run all test methods from the test case. Note that test_helper.rb is in the test directory, hence this directory needs to be added to the load path using the -I switch.
The . (dot) above indicates a passing test. When a test fails you see an F; when a test throws an error you see an E in its place. The last line of the output is the summary.
To see how a test failure is reported, you can add a failing test to the post_test.rb test case.
test "should not save post without title" do
  post = Post.new
  assert !post.save
end
Let us run this newly added test.
$ rake test test/models/post_test.rb test_should_not_save_post_without_title
F
Finished tests in 0.044632s, 22.4054 tests/s, 22.4054 assertions/s.
  1) Failure:
test_should_not_save_post_without_title(PostTest) [test/models/post_test.rb:6]:
Failed assertion, no message given.
1 tests, 1 assertions, 1 failures, 0 errors, 0 skips
In the output, F denotes a failure. You can see the corresponding trace shown under 1) along with the name of the failing test. The next few lines contain the stack trace followed by a message which mentions the actual value and the expected value by the assertion. The default assertion messages provide just enough information to help pinpoint the error. To make the assertion failure message more readable, every assertion provides an optional message parameter, as shown here:
test "should not save post without title" do
  post = Post.new
  assert !post.save, "Saved the post without a title"
end
Running this test shows the friendlier assertion message:
  1) Failure:
test_should_not_save_post_without_title(PostTest) [test/models/post_test.rb:6]:
Saved the post without a title
Now to get this test to pass we can add a model level validation for the title field.
class Post < ActiveRecord::Base
  validates :title, presence: true
end
Now the test should pass. Let us verify by running the test again:
$ rake test test/models/post_test.rb test_should_not_save_post_without_title
.
Finished tests in 0.047721s, 20.9551 tests/s, 20.9551 assertions/s.
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
Now, if you noticed, we first wrote a test which fails for a desired functionality, then we wrote some code which adds the functionality and finally we ensured that our test passes. This approach to software development is referred to as Test-Driven Development (TDD).
Many Rails developers practice Test-Driven Development (TDD). This is an excellent way to build up a test suite that exercises every part of your application. TDD is beyond the scope of this guide, but one place to start is with 15 TDD steps to create a Rails application.
To see how an error gets reported, here's a test containing an error:
test "should report error" do
  # some_undefined_variable is not defined elsewhere in the test case
  some_undefined_variable
  assert true
end
Now you can see even more output in the console from running the tests:
$ rake test test/models/post_test.rb test_should_report_error
E
Finished tests in 0.030974s, 32.2851 tests/s, 0.0000 assertions/s.
  1) Error:
test_should_report_error(PostTest):
NameError: undefined local variable or method `some_undefined_variable' for #<PostTest:0x007fe32e24afe0>
    test/models/post_test.rb:10:in `block in <class:PostTest>'
1 tests, 0 assertions, 0 failures, 1 errors, 0 skips
Notice the 'E' in the output. It denotes a test with error.
The execution of each test method stops as soon as any error or an assertion failure is encountered, and the test suite continues with the next method. All test methods are executed in alphabetical order.

3.3 What to Include in Your Unit Tests

Ideally, you would like to include a test for everything which could possibly break. It's a good practice to have at least one test for each of your validations and at least one test for every method in your model.

3.4 Available Assertions

By now you've caught a glimpse of some of the assertions that are available. Assertions are the worker bees of testing. They are the ones that actually perform the checks to ensure that things are going as planned.
There are a bunch of different types of assertions you can use. Here's an extract of the assertions you can use with minitest, the default testing library used by Rails. The [msg] parameter is an optional string message you can specify to make your test failure messages clearer. It's not required.
Assertion Purpose
assert( test, [msg] ) Ensures that test is true.
refute( test, [msg] ) Ensures that test is false.
assert_equal( expected, actual, [msg] ) Ensures that expected == actual is true.
refute_equal( expected, actual, [msg] ) Ensures that expected != actual is true.
assert_same( expected, actual, [msg] ) Ensures that expected.equal?(actual) is true.
refute_same( expected, actual, [msg] ) Ensures that expected.equal?(actual) is false.
assert_nil( obj, [msg] ) Ensures that obj.nil? is true.
refute_nil( obj, [msg] ) Ensures that obj.nil? is false.
assert_match( regexp, string, [msg] ) Ensures that a string matches the regular expression.
refute_match( regexp, string, [msg] ) Ensures that a string doesn't match the regular expression.
assert_in_delta( expecting, actual, [delta], [msg] ) Ensures that the numbers expected and actual are within delta of each other.
refute_in_delta( expecting, actual, [delta], [msg] ) Ensures that the numbers expected and actual are not within delta of each other.
assert_throws( symbol, [msg] ) { block } Ensures that the given block throws the symbol.
assert_raises( exception1, exception2, ... ) { block } Ensures that the given block raises one of the given exceptions.
assert_nothing_raised( exception1, exception2, ... ) { block } Ensures that the given block doesn't raise one of the given exceptions.
assert_instance_of( class, obj, [msg] ) Ensures that obj is an instance of class.
refute_instance_of( class, obj, [msg] ) Ensures that obj is not an instance of class.
assert_kind_of( class, obj, [msg] ) Ensures that obj is or descends from class.
refute_kind_of( class, obj, [msg] ) Ensures that obj is not an instance of class and is not descending from it.
assert_respond_to( obj, symbol, [msg] ) Ensures that obj responds to symbol.
refute_respond_to( obj, symbol, [msg] ) Ensures that obj does not respond to symbol.
assert_operator( obj1, operator, [obj2], [msg] ) Ensures that obj1.operator(obj2) is true.
refute_operator( obj1, operator, [obj2], [msg] ) Ensures that obj1.operator(obj2) is false.
assert_send( array, [msg] ) Ensures that executing the method listed in array[1] on the object in array[0] with the parameters of array[2 and up] is true. This one is weird eh?
flunk( [msg] ) Ensures failure. This is useful to explicitly mark a test that isn't finished yet.
Because of the modular nature of the testing framework, it is possible to create your own assertions. In fact, that's exactly what Rails does. It includes some specialized assertions to make your life easier.
Creating your own assertions is an advanced topic that we won't cover in this tutorial.

3.5 Rails Specific Assertions

Rails adds some custom assertions of its own to the test/unit framework:
Assertion Purpose
assert_difference(expressions, difference = 1, message = nil) {...} Test numeric difference between the return value of an expression as a result of what is evaluated in the yielded block.
assert_no_difference(expressions, message = nil, &amp;block) Asserts that the numeric result of evaluating an expression is not changed before and after invoking the passed in block.
assert_recognizes(expected_options, path, extras={}, message=nil) Asserts that the routing of the given path was handled correctly and that the parsed options (given in the expected_options hash) match path. Basically, it asserts that Rails recognizes the route given by expected_options.
assert_generates(expected_path, options, defaults={}, extras = {}, message=nil) Asserts that the provided options can be used to generate the provided path. This is the inverse of assert_recognizes. The extras parameter is used to tell the request the names and values of additional request parameters that would be in a query string. The message parameter allows you to specify a custom error message for assertion failures.
assert_response(type, message = nil) Asserts that the response comes with a specific status code. You can specify :success to indicate 200-299, :redirect to indicate 300-399, :missing to indicate 404, or :error to match the 500-599 range
assert_redirected_to(options = {}, message=nil) Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial, such that assert_redirected_to(controller: "weblog") will also match the redirection of redirect_to(controller: "weblog", action: "show") and so on.
assert_template(expected = nil, message=nil) Asserts that the request was rendered with the appropriate template file.
You'll see the usage of some of these assertions in the next chapter.

Comments