ActiveSupport_TestCase + shoulda によるテスト のバックアップソース(No.2)

更新

[[公開メモ]]

#contents

* ActiveSupport::TestCase + shoulda による Ruby on Rails のテスト [#ge5f5cd1]

- ActiveSupport::TestCase は Rails 標準のテストフレームワーク
- [[shoulda>https://github.com/thoughtbot/shoulda]] は読みやすいテストケースを書くための拡張

これらを使って Rails アプリケーションをテストするためのノウハウをメモとして残したい

* いろいろ検討中 [#rc1df26c]

まだちゃんとしたテストは書き始めたばかりなので全然やり方が定まってません。
このレベルの物をこんなところに晒すのも何なのですが、とりあえずメモとして書いちゃいます。

** コントローラのテスト [#m9c4f2cd]

途中ロケールの処理が入っていますが、そのあたりの構成は 
[[ソフトウェア/rails/国際化]] に書いた通りです。

test/controllers/account_controller_test.rb
 LANG:ruby(linenumber)
 #coding: utf-8
 require 'test_helper'
 
 class AccountControllerTest < ActionController::TestCase
   tests AccountController   # この例のように標準的な命名をしている限り、
                             # コントローラ名の指定はしなくてもOK
 
   # すべての context に共通の初期化処理
   setup {
 
     clear_all
 
     # ActionController::TestCase にて、以下のクラス変数が使われている。
     #
     #   @@required_fixture_classes: false
     #   @@already_loaded_fixtures: {}
     #   @@test_suites: {ActiveSupport::TestCase=>true, ...}
     #   @@current: #<AccountControllerTest:0xb192174>
     #
     # クラス変数を使う場合は、これらおよび未来の TestCase のクラス変数と
     # 名前が被らないよう注意する必要あり
 
     # setup とテストケース should の中とで情報をやりとりするのに
     # 普通にインスタンス変数 @xxx を使うか、反則技でクラス変数 @@xxx を
     # 使うのが良いか迷い中
     #
     # インスタンス変数を使うのが普通だが、
     #   - クラススコープからインスタンス変数にアクセスできなくて
     #     残念に感じることがたまにある
     #   - 比較的多数のインスタンス変数がフレームワークによって
     #     使われているため、命名の衝突が怖い
     #
     # クラス変数を使うのはかなり異端
     #   - 本来の目的からずれまくってるので完全に反則技
     #   - 初期化し忘れると直前のテストケースの値が残ってしまうのも問題
     #   - その代わり、どこからでもアクセスできるのがかなり便利?
 
     @@valid_account_params = { 
       name: "Foo Baa", 
       email: "account@example.com",
       affiliation: "My institute",
       address: "My address",
       telephone: "+00-000-0000"
     }
     
     @@invalid_account_params = {
       name: ''
     }
     
     @@valid_registration_params = {
       gender: 1,
       membership: 1,
       student: false,
       receipt_address: 'My address',
       payaccount: 'My Account'
     }
     
     # インスタンス変数として以下が定義されるので、名前が
     # 被らないように注意が必要
     #
     # インスタンス変数      利用時の記法   内容
     # @__name__             @__name__      現在実行中のテストのタイトルが入る
     # @__io__
     # @passed
     # @_assertions
     # @fixture_cache
     # @fixture_connections
     # @loaded_fixtures
     # @tagged_logger
     # @_partials
     # @_templates
     # @_layouts
     # @_files
     # @request              request        #<ActionController::TestRequest>
     # @response             response       #<ActionController::TestResponse>
     # @controller           @controller    #<Controller under testing>
     # @routes               @routes        #<ActionDispatch::Routing::RouteSet>
     # @shoulda_context
     # @html_document        html_document  #<HTML::Document>
     # @assigns              assigns        @controller 内で定義されたインスタンス変数
   }
 
   def clear_all
     ActionMailer::Base.deliveries.clear
     DatabaseRewinder.clean_all
     session.clear
     cookies.clear
   end
  
   # 以下すべてを ja, en 両方のロケールで試す
   [:ja,:en].each do |locale|
     context "with locale #{locale}," do
       setup {
         request.env['HTTP_ACCEPT_LANGUAGE'] = locale.to_s
       }
       
       # ここからが本当のテスト内容
       
       context "without login," do
 
         context "GET #new" do
           setup { 
             get :new 
           }
           should respond_with :success
           should render_template :new
           should_not set_the_flash
         end
 
         context 'GET #home' do
           setup { 
             get :home 
           }
           should redirect_to_path :account_login
           should set_the_flash # login first
           should 'set session_intended_path to requested path' do
             # セッションキーは ApplicationController で 
             #   @session_intended_path="hogehoge"
             # として与えられているので、assigns を使って読み取る
             assert_equal @request.path, 
                          session[assigns(:session_intended_path)]
           end
         end
         
         context 'invalid POST #create' do
           setup { 
             post :create, account: @@invalid_account_params
           }
           should "not create account" do
             assert_equal 0, Account.count
           end
           should set_the_flash  # already login
           should render_template :new
         end
         
         context 'valid POST #create' do
           setup {
             post :create, account: @@valid_account_params
           }
           should "create account" do
             assert_equal 1, Account.count
           end
           should set_the_flash  # account created
           should redirect_to_path :account_login
           should "send email to registered address" do
             assert_not ActionMailer::Base.deliveries.empty?
             email_sent = ActionMailer::Base.deliveries.last
             assert_equal @@valid_account_params[:email], email_sent.to
           end
         end
         
       end
 
       context "with auto-login," do
 
         setup {  
           @@account = Account.create!(
             @@valid_account_params.merge(
               password: Account.generate_new_password,
               auto_login: Account.generate_new_password
             )
           )
           
           cookies[assigns(:cookie_auto_login)] =
               [@@account.auto_login, @@account.email].join(':')
         }
         
         context 'GET #home' do
           setup { 
             get :home 
           }
           should respond_with :success
           should 'update autologin password at successful auto login' do
             assert_not_equal @@account.auto_login, assigns[:account].auto_login
             assert_equal assigns[:account].auto_login, 
                          cookies[assigns[:cookie_auto_login]].split(':',2)[0]
           end
         end
         
         context 'GET #new' do
           setup { 
             get :new
           }
           should set_the_flash  # already login
           should redirect_to_path :account
         end
         
         context 'GET #edit' do
           setup { 
             get :edit
           }
           should respond_with :success
         end
         
         context 'POST #update' do
           setup {
             post :update, account: {name: "Updated Name"}
           }
           should 'update name' do
             account = Account.find_by_email(@@account.email)
             assert_equal "Updated Name", account.name
           end
           should set_the_flash  # successfully updated
           should redirect_to_path :account
         end
         
         context 'DELETE #destroy' do
           setup {
             delete :destroy
           }
           should "delete account" do
             assert_equal 0, Account.count
           end
           should set_the_flash  # successfully deleted
           should redirect_to_path :default
         end
         
         context 'with valid registration,' do
           setup {
             @@account.registration =
               Registration.create!(@@valid_registration_params)
           }
 
           context 'DELETE #destroy' do
             setup {
               delete :destroy
             }
             should "not delete account" do
               assert_equal 1, Account.count
             end
             should set_the_flash  # can not delete
             should redirect_to_path :account
           end
         end
 
       end
     end
   end
   
 end

* 独自拡張を読み込む [#xdb96442]

test/test_helper.rb
 LANG:ruby
 ...
 
 require 'test_i18n_translation_strictly'
 Dir[File.expand_path('../shoulda_macros', __FILE__) << '/*.rb'].each do |file|
   require file
 end
 
 ...
 

** I18n.translate の翻訳の抜けを検出する [#ub2cc8f0]

config/initializers/test_i18n_translation_strictly.rb
 LANG:ruby(linenumber)
 if ENV['RAILS_ENV'] == "test"
 
 # 翻訳が見付からないときに例外を投げる
 #   I18n.exception_handler
 #   http://guides.rubyonrails.org/i18n.html
 # これだけではモデル名やカラム名の翻訳は無くても
 # エラーにならないので、
 #   ActiveRecord::model_name.human
 #       https://github.com/rails/rails/blob/master/activemodel/lib/active_model/translation.rb#L43
 #   ActiveRecord::human_attribute_name
 #       https://github.com/rails/rails/blob/master/activemodel/lib/active_model/naming.rb#L175
 # にも手を入れる。locale == :en では、モデル名やカラム名を
 # そのまま使えることも多いので、それ以外の時に翻訳が未設定
 # であれば例外を投げる
 #
 # 後者2つについては関数のコードをほぼ丸々コピーしているので、
 # 元のコードが変更されたらそれに合わせて変更しなければならない
 
 module I18n
   class JustRaiseExceptionHandler < ExceptionHandler
     def call(exception, locale, key, options)
       if exception.is_a?(MissingTranslation) and
           key.to_s != 'i18n.plural.rule'
         raise exception.to_exception
       else
         super
       end
     end
   end
 end
 I18n.exception_handler = I18n::JustRaiseExceptionHandler.new
 
 module ActiveModel
   class Name
     def human(options={})
       return @human unless @klass.respond_to?(:lookup_ancestors) &&
                            @klass.respond_to?(:i18n_scope)
 
       defaults = @klass.lookup_ancestors.map do |klass|
         klass.model_name.i18n_key
       end
 
       defaults << options[:default] if options[:default]
 #      defaults << @human
       defaults << @human if I18n.locale == :en
       
       options = { scope: [@klass.i18n_scope, :models], count: 1, default: defaults }.merge!(options.except(:default))
       I18n.translate(defaults.shift, options)
     end
   end
 end
 
 module ActiveModel
   module Translation
     def human_attribute_name(attribute, options = {})
       options = { count: 1 }.merge!(options)
       parts = attribute.to_s.split(".")
       attribute = parts.pop
       namespace = parts.join("/") unless parts.empty?
       attributes_scope = "#{self.i18n_scope}.attributes"
 
       if namespace
         defaults = lookup_ancestors.map do |klass|
           :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}"
         end
         defaults << :"#{attributes_scope}.#{namespace}.#{attribute}"
       else
         defaults = lookup_ancestors.map do |klass|
           :"#{attributes_scope}.#{klass.model_name.i18n_key}.#{attribute}"
         end
       end
 
       defaults << :"attributes.#{attribute}"
       defaults << options.delete(:default) if options[:default]
 #      defaults << attribute.humanize
       defaults << attribute.humanize if I18n.locale == :en
 
       options[:default] = defaults
       I18n.translate(defaults.shift, options)
     end
   end
 end
 
 end

** should redirect_to_path [#r6fcffc4]

test/shoulda_macros/action_controller_macros.rb
 LANG:ruby(linenumber)
 class ActionController::TestCase
 
   # shoulda の redirect_to では、
   #   should redirect_to account_path
   #   should redirect_to account_path(action: :edit)
   #   should redirect_to url_for(controller: :account, action: :edit)
   # などとは書けないので、かわりに
   #   should redirect_to("account_path"){ account_path }
   #   should redirect_to("account_path(action: :edit)"){ account_path(action: :edit) }
   #   should redirect_to("controller: :account, action: :edit"){ 
   #               @controller.url_for(controller: :account, action: :edit) }
   # などと書かなければならない。より簡単に
   #   should redirect_to_path :account
   #   should redirect_to_path :account, action: :edit
   #   should redirect_to_path controller: :account, action: :edit
   # などと書けるようにするのがこの関数
   def self.redirect_to_path(*arg)
     if arg.first.is_a? Symbol
       redirect_to_path_arg = arg.dup
       redirect_to_path_symbol = :"#{redirect_to_path_arg.shift}_path"
       if redirect_to_path_arg.empty?
         redirect_to(redirect_to_path_symbol){
           __send__(redirect_to_path_symbol)
         }
       else
         redirect_to_path_arg_s =redirect_to_path_arg.inspect.gsub(/\A\[|\]\z/,'')
         redirect_to_path_arg_s.gsub!(/\A\{|\}\z/,'') if redirect_to_path_arg.count == 1
         redirect_to("#{redirect_to_path_symbol}(#{redirect_to_path_arg_s})", &Proc.new {
           __send__(redirect_to_path_symbol, *redirect_to_path_arg)
         })
       end
     else
       redirect_to_path_arg = arg.dup
       redirect_to_path_arg_s =redirect_to_path_arg.inspect.gsub(/\A\[|\]\z/,'')
       redirect_to_path_arg_s.gsub!(/\A\{|\}\z/,'') if redirect_to_path_arg.count == 1
       redirect_to(redirect_to_path_arg_s){
         @controller.url_for *redirect_to_path_arg
       }
     end
   end
 
 end

* コメント [#yfa709ad]

#article_kcaptcha

Counter: 8375 (from 2010/06/03), today: 1, yesterday: 0