Wednesday, September 30, 2009

Rails でテストするのはどうしようかと調べてみる

動機づけ

Railsアプリケーションを作ってみよう。せっかくなのでTest Drivenで。
とりあえず,ここから(A Guide to Testing Rails Applications)読みはじめてみる。訳は適当で,その場その場で気になったことをメモしている程度

1 Why Write Tests for your Rails Applications?

なぜTestを書くのか。

  • Railsはテストを書くのを楽にしてくれるよ。ModelやControllerを作成時に,テストコードのスケルトンも一緒に作ってくれるので,そこからスタートできる。
  • Railsのテスト実行は簡単。リファクタリングしても機能を満たしていることを確認できる。
  • ブラウザのリクエストをシミュレートできるので,ブラウザ使わずにテストできる(ここ重要。)。

2 Introduction to Testing

"To write efficient tests, you’ll need to understand how to set up this database and populate it with sample data."

2.1 The Three Environments

config/database.yml をみると,

  • production
  • development
  • test
と,3つの環境が用意されている。こうすることで,test環境とproduction環境を分けることで,テストデータが実行環境をおかす心配はない。
「rake db:test:prepare で,テスト用データベース環境の構築ができる」。

2.2 Rails Sets up for Testing from the Word Go

test フォルダの下にできるfolder

unit
tests for your models
functional
tests for your controllers
integration
test that invole any anumber of controllers interacting
fixtures
a way of organzizing test date

2.3 The Low-Down on Fixtures

Fixures: a fancy word for sample data.
Here’s a sample YAML fixture file:

# low & behold! I am a YAML comment! 
david: 
  name: David Heinemeier Hansson 
  birthday: 1979-10-15 
  profession: Systems development 

steve: 
  name: Steve Ross Kellock 
  birthday: 1974-09-27 
  profession: guy with keyboard 
ERb allosw you embed ruby code within templates. <% %>タグはRuby codeと認識される。
Fixtures in Action
デフォルトでは text/fixtures 以下にある全てのfixturesを
  • unit test/functional test時に 読みこむ。
  • Remove any exsiting data from the table corresponding to the fixture
  • Load the fixture data into the table
  • Dump the fixures data into a variable in case you want to access it directly
Hashes with Special Powers
Fixtures は 基本的にはハッシュ objectなので,このようなアクセスの方法があるよ。
# this will return the Hash for the fixture named david 
users(:david) 
# this will return the property for david called id 
users(:david).id 
Fixturesからオリジナルのクラスに変換することができるので,そのクラスに許されているメソッドを実行できる。
# using the find method, we grab the "real" david as a User 
david = users(:david).find

# and now we have access to methods only available to a User class 
email(david.girlfriend.email, david.location_tonight) 

3. Unit Testing your Models

まず Test 用の Rails プロジェクト (TestingRails) を作成しておく。


/home/abekatsu/webroot% rails TestingRails
      create  
      create  app/controllers
      create  app/helpers
      create  app/models
      create  app/views/layouts
      create  config/environments
      create  config/initializers
      create  config/locales
      create  db
      create  doc
      create  lib
      create  lib/tasks
      create  log
      create  public/images
      create  public/javascripts
      create  public/stylesheets
      create  script/performance
      create  test/fixtures
      create  test/functional
      create  test/integration
      create  test/performance
      create  test/unit
      create  vendor
      create  vendor/plugins
      create  tmp/sessions
      create  tmp/sockets
      create  tmp/cache
      create  tmp/pids
      create  Rakefile
      create  README
      create  app/controllers/application_controller.rb
      create  app/helpers/application_helper.rb
      create  config/database.yml
      create  config/routes.rb
      create  config/locales/en.yml
      create  config/initializers/backtrace_silencers.rb
      create  config/initializers/inflections.rb
      create  config/initializers/mime_types.rb
      create  config/initializers/new_rails_defaults.rb
      create  config/initializers/session_store.rb
      create  config/environment.rb
      create  config/boot.rb
      create  config/environments/production.rb
      create  config/environments/development.rb
      create  config/environments/test.rb
      create  script/about
      create  script/console
      create  script/dbconsole
      create  script/destroy
      create  script/generate
      create  script/runner
      create  script/server
      create  script/plugin
      create  script/performance/benchmarker
      create  script/performance/profiler
      create  test/test_helper.rb
      create  test/performance/browsing_test.rb
      create  public/404.html
      create  public/422.html
      create  public/500.html
      create  public/index.html
      create  public/favicon.ico
      create  public/robots.txt
      create  public/images/rails.png
      create  public/javascripts/prototype.js
      create  public/javascripts/effects.js
      create  public/javascripts/dragdrop.js
      create  public/javascripts/controls.js
      create  public/javascripts/application.js
      create  doc/README_FOR_APP
      create  log/server.log
      create  log/production.log
      create  log/development.log
      create  log/test.log

Railsにおいて,Unit Testとはモデル(Model)のテストを書くことである。まずは,モデルpostをscaffoldで構築してみて,その際にできるtest suiteについて見てみる。

/home/abekatsu/webroot/TestingRails% script/generate scaffold post test:string body:text
      exists  app/models/
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/posts
      exists  app/views/layouts/
      exists  test/functional/
      exists  test/unit/
      create  test/unit/helpers/
      exists  public/stylesheets/
      create  app/views/posts/index.html.erb
      create  app/views/posts/show.html.erb
      create  app/views/posts/new.html.erb
      create  app/views/posts/edit.html.erb
      create  app/views/layouts/posts.html.erb
      create  public/stylesheets/scaffold.css
      create  app/controllers/posts_controller.rb
      create  test/functional/posts_controller_test.rb
      create  app/helpers/posts_helper.rb
      create  test/unit/helpers/posts_helper_test.rb
       route  map.resources :posts
  dependency  model
      exists    app/models/
      exists    test/unit/
      exists    test/fixtures/
      create    app/models/post.rb
      create    test/unit/post_test.rb
      create    test/fixtures/posts.yml
      create    db/migrate
      create    db/migrate/20090929002133_create_posts.rb

モデルに関するテスト箇所は,

      create    app/models/post.rb
      create    test/unit/post_test.rb
      create    test/fixtures/posts.yml

test/unit/post_test.rbを見てみる。

require 'test_helper'

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

次の
  test "the truth" do
    assert true
  end
がガイドと違うところ。ActiveSupport::TestCaseを見てもよくわからない。ひとまず,気にしないでおいておく。
テスト用クラスは ActiveSupport::TestCase の 子クラス。テスト内容は

  assert true

のように,trueがtrueであるかないか。だから,かならず成功しなければいけないテストが含んでいる。次節に進んでみる。

3.1 Preparing your Application for Testing

まずDBの構造を最新にしておくために "rake db:migrate" を実行する。


/home/abekatsu/webroot/TestingRails% rake db:migrate
(in /home/abekatsu/webroot/TestingRails)
==  CreatePosts: migrating ====================================================
-- create_table(:posts)
   -> 0.0019s
==  CreatePosts: migrated (0.0021s) ===========================================



"db/scheme.rb" ができていることを確認して,"rake db:test:load" で,テスト用データベースを作成する。いつの時点でも,"rake db:test:load" でテスト用データベースを再構築することができる。
この段階で,"rake db:test:prepare" を実行できる。

/home/abekatsu/webroot/TestingRails% rake db:test:load
(in /home/abekatsu/webroot/TestingRails)
/home/abekatsu/webroot/TestingRails% rake db:test:prepare
(in /home/abekatsu/webroot/TestingRails)

テスト用にアプリケーションを準備するためのRake Tasksはになって示されている。
実際にテストを実行してみる。例では "ruby unit/post_test.rb -n test_truth" と test method を指定しているが,上記ではメソッド "test_truth" を定義していないので "ruby unit/post_test.rb" を実行してみる。

/home/abekatsu/webroot/TestingRails% cd test 
/home/abekatsu/webroot/TestingRails/test% ruby unit/post_test.rb 
Loaded suite unit/post_test
Started
.
Finished in 0.202105 seconds.

1 tests, 1 assertions, 0 failures, 0 errors
/home/abekatsu/webroot/TestingRails/test% 

次に,"タイトルがないポストを保存してはならないテスト(test_should_not_save_post_without_title)" を作成する。"unit/post_test.rb" に次のテストを追加する。
  test "post should not save without title" do
    post = Post.new
    assert !post.save
  end
今のところ,モデルPostはいじっていないので,このテストは必ず失敗するはず。実行してみる。

/home/abekatsu/webroot/TestingRails/test% ruby unit/post_test.rb
Loaded suite unit/post_test
Started
F.
Finished in 0.21082 seconds.

  1) Failure:
test_post_should_not_save_without_title(PostTest) [unit/post_test.rb:11]:
 is not true.

2 tests, 2 assertions, 1 failures, 0 errors

test_post_should_not_save_without_title(PostTest) [unit/post_test.rb:11] が失敗していることを確認した。もうちょっとかっこよくtestを表示させてみたいので,
  test "post should not save without title" do
    post = Post.new
    assert !post.save, "Saved the post without a title"
  end
とassert文に追加して,テスト実行。ちょっとかっこいい。
/home/abekatsu/webroot/TestingRails/test% ruby unit/post_test.rb
Loaded suite unit/post_test
Started
F.
Finished in 0.262242 seconds.

  1) Failure:
test_post_should_not_save_without_title(PostTest) [unit/post_test.rb:11]:
Saved the post without a title.  <== "Saved the post without a title" が表示されている。
 is not true.

2 tests, 2 assertions, 1 failures, 0 errors
/home/abekatsu/webroot/TestingRails/test% 
それでは,このテストが通るように,モデルPostを編集してみる。編集するファイルは "app/models/post.rb"
"app/models/post.rb"を次のように編集する。titleが存在していないと保存できないようにするために "validates_presence_of :title" を追加する。

class Post < ActiveRecord::Base
  validates_presence_of :title
end
"validates_presence_of" は ActiveRecord::Validations::ClassMethodsで定義されているメソッド。引数はsymbol。ここでは "title"。なぜ ActiveRecord::Validations::ClassMethods をここで呼ぶことができるのかがわからない。
再びテストを実行してみる。

/home/abekatsu/webroot/TestingRails/test% ruby unit/post_test.rb
Loaded suite unit/post_test
Started
..
Finished in 0.189441 seconds.

2 tests, 2 assertions, 0 failures, 0 errors
今度はテストが成功したことがわかる。
To see how an error gets reported, here's a test containing an error(うまく日本語に訳せない。)再び "post_test.rb" に戻って,

  test "test sholud report error" do
    some_undefined_variable 
    assert true
  end
を追加して,テストを実行してみる。

/home/abekatsu/webroot/TestingRails/test% ruby unit/post_test.rb             
Loaded suite unit/post_test
Started
.E.
Finished in 0.191917 seconds.

  1) Error:
test_test_sholud_report_error(PostTest):
NameError: undefined local variable or method `some_undefined_variable' for #
    unit/post_test.rb:15:in `test_test_sholud_report_error'

3 tests, 2 assertions, 0 failures, 1 errors
ここまでで 3.2 節。次節はUnit Test

わからないこと

  • なぜ validates_of が ActiveRecord::Base 内で呼ぶことができるのか?
  • "the truth" と書かれたところだけテストを実行してみたいのだが,どのように指定しればいいのかよくわからない。全てのテストはパスさせなければいけないから,そんなことを考えるのは無意味だろうか。

その他

15 TDD steps to create a Rails applicationというのが公開されている。

1 comment:

abekatsu said...

"%GEM_PATH%/ruby/1.8/gems/activerecord-2.3.3/lib/active_record.rb" を読むと,ActiveRecords::Validations モジュールを読むように設定されている。( autoload :Valid
ations, 'active_record/validations')。

そこで ("%GEM_PATH%/ruby/1.8/gems/activerecord-2.3.3/lib/active_record/validations.rb"),module ActiveRecord::Validations::ClassMethodsが定義されているので,validates_presense_of を呼ぶことができる。

これで納得がいった。