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.
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
end
|
A line by line examination of this file will help get you oriented to Rails testing code and terminology.
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.
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
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, &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
Post a Comment