7.8. Testing Models

Testing is an integral part of developing in Rails. The subject matter is quite large and this book isn't even going to begin to do it enough justice. Nevertheless, it would do you a great disservice to close a chapter on ActiveRecord without at least scratching the bare surface of unit testing.

It is called unit testing, because individual units of code (for example, models) are tested to verify that they work correctly.

The Rails community embraces Test-Driven Development (TDD) and many developers even go as far as to write tests first, so that they act as a well-defined spec, and only then write code to pass those tests (a practice commonly known as Test-First Development).

Lately many developers have been embracing BDD (Behavior-Driven Development), often by employing the excellent RSpec. I recommend that, with the help of Google, you check out this alternative testing framework.

Many people dislike testing because they tend to prefer to spend their time writing "real code." But the reality is that, aside from being a staple of the XP methodology, testing improves the quality of your application. Testing cannot guarantee a complete lack of bugs in your Web application, but with good testing coverage you can ensure that many basic cases/functionalities are properly handled.

More importantly, it gives you confidence when it comes time to make changes and develop the application further, without having to worry that a change may break a related piece of code somewhere else in the code repository. With a good set of tests, you'll be able to spot "what broke what," in many cases.

Other secondary, positive side-effects include the fact that tests provide a form of documentation for how your application is supposed to work; they can force programmers to think more thoroughly about the code they are about to write, and they can also be a warning sign for problematic code. In fact, it's been my experience that code that is hard to test is often code that could be refactored and better thought through.

Testing has a bit of a learning curve to it, but the payoff far outweighs the initial time investment. And if you are used to the NUnit framework with .NET, it will all be rather familiar, while at the same time you'll be able to appreciate how much nicer it is to work with Ruby's unit testing library. In fact, ActiveRecord essentially relies on Ruby's Test::Unit framework, which ships as part of the Standard Library.

You are going to operate on the sample blog application again.

The first thing that you'll do is run the unit tests. From the main folder of the project, run the following task:

rake test:units

The output of the command should be similar to the following:

(in C:/projects/blog)
c:/ruby/bin/ruby -Ilib;test "c:/ruby/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/
rake_test_loader.rb" "test/unit/article_test.rb" "test/unit/comment_test.rb"
Loaded suite c:/ruby/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
Started
..
Finished in 0.593 seconds.

2 tests, 2 assertions, 0 failures, 0 errors

For each test that passes, a dot is displayed beneath the word Started. At the end of the report, a line summarizes the outcome after you've run the test suite. In this case, it informs you that there were two tests, two assertions, and no failures or errors.

Tests are public methods (defined in a test case) that contain one or more assertions, whereas assertions are comparisons between an expected value and the value of an expression. These either pass or fail, depending on whether the assertion is actually true or false during the execution of the test suite. If a test causes an unrescued exception, this outcome is reported as an error, as opposed to a failure.

The aim is to have zero failures and errors. When all the tests have passed successfully, you can go ahead and change the application or add a new feature (and add new tests for it). If upon running the tests again the application fails this time around, you'll know for sure that your change or new feature was problematic. When there are failed assertions, the value expected by the assertion and the actual value are both shown. If there are errors, the error message and stacktrace are displayed in the output as well.

Let's take a look at the two tests and assertions that you've just passed, by opening the files article_test.rb and comment_test.rb in the project's testunit directory:

# testunitarticle_test.rb
require 'test_helper'

class ArticleTest < ActiveSupport::TestCase
  # Replace this with your real tests.
  test "the truth" do
    assert true
  end
end# testunitcomment_test.rb
require 'test_helper'

class CommentTest < ActiveSupport::TestCase
  # Replace this with your real tests.
  test "the truth" do
    assert true
  end
end

The test case is a subclass of ActiveSupport::TestCase and the method test is passed a string literal that acts as a label for the test. With the method's block there is just a placeholder with a single assertion: assert true. assert is a method that defines an assertion that always passes except when the argument is false, nil, or raises an error. If you change article_test.rb so that the test body is assert false and run the tests again, you would obtain one failure, an F instead of a dot, and the message <false> is not true. Changing it to assert 3/0 would give you one error, an E instead of a dot, and the error message ZeroDivisionError: divided by 0 (as well as the stacktrace).

assert false is equivalent to the method flunk, which always fails.

This is the general structure of a test case file in a Rails setting:

require 'test_helper'

class NameTest < ActiveSupport::TestCase
  fixtures :models

  # Run before each test method
  def setup
    # ...
  end

  test "a functionality" do
    # assertions
    # ...
  end

  # Run after each test method
  def teardown
    # ...
  end
end

The code within the optional method setup will be executed before each test method, whereas teardown is invoked after each test. Because you are testing models, and doing assertions is all about comparing the evaluated expressions against known values, you'll need a way to store all the well-known records that are not subject to change in the test database. The perfect answer to this can be found in fixtures, which are loaded through the fixtures method.

If you take a look at the testfixtures directory within the project, you'll notice that two fixture files were generated by the scaffold generator: articles.yml and comments.yml. These are YAML files that can contain several records that you'd like to load into the database before running the tests. These records have a name, and besides being available to you when reading the test database, they can also be accessed through a hash. This is important when you want to verify the correctness of the data obtained through a model.

This is what articles.yml looks like (as you can see it respects the table structure of articles):

# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html


one:
  title: MyString
  body: MyText
  published: false
  published_at: 2008-07-10 22:46:42

two:
  title: MyString
  body: MyText
  published: false
  published_at: 2008-07-10 22:46:42

Notice that the indentation is arbitrary, as long as it's consistent. It's still good idea to indent by two spaces as per usual in Ruby.

The two records, one and two, are statically defined, but YAML fixtures allow you to embed ERb code with the usual <% %> and <%= %> pairs. Therefore, it's possible to dynamically define more records or define their values programmatically. CSV fixtures are also permitted, in which case the record names are auto-generated. However, YAML is the default and generally favored format.

Let's change the articles fixtures into something less repetitive and specify the id while you're at it (because this would be random otherwise):

hello:
  id: 1
  title: Hello, World!
  body: puts "Hello, world!"
  published: true
  published_at: 2008-09-08 05:30:22

fall:
  id: 2
  title: Fall is coming
  body: Some interesting text
  published: false
  published_at: 2008-09-08 07:48:12

Now load these fixtures by adding fixtures :article to the ArticleTest test case.

As you can imagine, the single test that you have right now is pretty useless, so you'll get rid of it and instead write a couple of tests that are more meaningful, using a few of the many assertion methods available.

Consult the Ruby documentation for Test::Unit for a list of available assertions. A few common ones are assert, assert_nil, assert_equal, assert_raise, and assert_not_raised. All the assertion methods accept an optional argument to specify a custom message.

To get started you can test that the validations are working as follows:

test "article validations" do
    no_title_or_body = Article.new(:published => true, :published_at => Time.now)

    no_title = Article.find(1)
    no_title.title = nil

    duplicate_body = Article.find(2)
    duplicate_body.body = articles(:hello).body

assert_equal false, no_title_or_body.save

    assert_raise ActiveRecord::RecordInvalid do
      no_title.save!
    end

    assert_raise ActiveRecord::RecordInvalid do
      duplicate_body.save!
    end
  end

Notice how you can access the named records defined in the fixtures file by passing their symbol to the name of the fixtures (for example, articles(:hello)).

The Article model defines validations that prevent objects from being saved when they're missing a body or title, or if the body already exists. The test instantiates a record without a body and title, so it's fair to expect that invoking save will lead to false being returned. You assert this with:

assert_equal false, no_title_or_body.save

It then retrieves an existing record and assigns nil to its title. Again, this is in direct violation of the validation you defined in the model so invoking save! should lead to an ActiveRecord::RecordInvalid error being raised. You assert this with assert_raise, which accepts the error class and a block that is supposed to raise that error type:

assert_raise ActiveRecord::RecordInvalid do
  no_title.save!
end

Note that save would not raise any errors, but rather quietly return false.

Finally, the test retrieves another record and assigns the value of the first record to its body attribute. Because you can't have two records with the same body, you'll assert that you expect the call to save! to raise an ActiveRecord::RecordInvalid error:

assert_raise ActiveRecord::RecordInvalid do
  duplicate_body.save!
end

ActiveRecordError is the generic exception class. All the error classes defined by ActiveRecord inherit from it, which in turn inherits from Ruby's StandardError.

Now you can add a test for the published and unpublished named scopes:

test "article status" do
  published_article = Article.find_by_published(true)
  unpublished_article = Article.find_by_published(false)
  scheduled_article = Article.create(:title => "A post in the future",
                                     :body => "... some text ...",
                                     :published => true,
                                     :published_at => 2.hours.from_now)

assert Article.published.include?(published_article)
  assert Article.unpublished.include?(unpublished_article)
  assert Article.unpublished.include?(scheduled_article)
  assert_equal false, Article.published.include?(unpublished_article)
  assert_equal false, Article.published.include?(scheduled_article)
  assert_equal false, Article.unpublished.include?(published_article)
end

The test first retrieves two records, respectively: a published and an unpublished one, and then creates a new scheduled one (published with a future publication date). Then you assert that the published one should be included in the array returned by the published named scope, and the unpublished and scheduled ones should be included in the array returned by the unpublished named scope. And for good measure, you'll also make sure it checks that neither of the named scopes include an inappropriate record (for example, you don't want a scheduled post to appear in the published list).

The resulting test case is shown in Listing 7-5.

Example 7.5. The ArticleTest Test Case with a Couple of Tests
require 'test_helper'

class ArticleTest < ActiveSupport::TestCase
  fixtures :articles

  test "article validations" do
    no_title_or_body = Article.new(:published => true, :published_at => Time.now)

    no_title = Article.find(1)
    no_title.title = nil

    duplicate_body = Article.find(2)
    duplicate_body.body = articles(:hello).body

    assert_equal false, no_title_or_body.save

    assert_raise ActiveRecord::RecordInvalid do
      no_title.save!
    end

    assert_raise ActiveRecord::RecordInvalid do
      duplicate_body.save!
    end
  end

  test "article status" do
    published_article = Article.find_by_published(true)
    unpublished_article = Article.find_by_published(false)
    scheduled_article = Article.create(:title => "A post in the future",
                                       :body => "... some text ...",
                                       :published => true,
                                       :published_at => 2.hours.from_now)

assert Article.published.include?(published_article)
    assert Article.unpublished.include?(unpublished_article)
    assert Article.unpublished.include?(scheduled_article)
    assert_equal false, Article.published.include?(unpublished_article)
    assert_equal false, Article.published.include?(scheduled_article)
    assert_equal false, Article.unpublished.include?(published_article)
  end
end

When the setup logic is common among a few tests, it's better to refactor the test case so that the setup logic is moved to the setup method, which is invoked before the execution of each test case.

Running the tests you now obtain:

C:projectslog> rake test:units
(in C:/projects/blog)
c:/ruby/bin/ruby -Ilib;test "c:/ruby/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/
rake_test_loader.rb" "test/unit/article_test.rb" "test/unit/comment_test.rb"
Loaded suite c:/ruby/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader

Started
...
Finished in 0.354 seconds.

3 tests, 10 assertions, 0 failures, 0 errors

Great! Keep in mind that this is not by any means a complete set of tests, and you'd probably want more records in your fixtures to play around with. But the example should be enough to get you started.

You can check some interesting stats about your application, including the code-to-test ratio, by running the rake task stats.

Unit Testing and Transactions

By default the execution of each test is wrapped in a transaction that is rolled back upon completion. This has two positive consequences. The first is related to performance. This approach doesn't require you to reload the fixture data after each test, which speeds up the execution of the whole test suite considerably. Second, and perhaps more importantly, each test will start with the same data in the database, therefore eliminating the risk of creating dependencies between different tests, where one test changes the data and the other needs to be aware of it in order to work. That'd be a very bad approach to testing and a route that you definitely don't want to go down.


As a final exercise, try to create additional tests for Article and perhaps move onto the Comment model's tests as well. You'll notice that the fixtures within comments.yml have an article article key. You can assign the name of an article fixture (for example, hello) to that column as follows:

article: hello

You'll get back to the subject of testing in the next chapter, which discusses functional and integration tests.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.116.60.158