ActiveSupport_TestCase + shoulda によるテスト の履歴(No.2)
更新- 履歴一覧
- 差分 を表示
- 現在との差分 を表示
- ソース を表示
- ソフトウェア/rails/ActiveSupport_TestCase + shoulda によるテスト へ行く。
ActiveSupport::TestCase + shoulda による Ruby on Rails のテスト†
- ActiveSupport::TestCase は Rails 標準のテストフレームワーク
- shoulda は読みやすいテストケースを書くための拡張
これらを使って Rails アプリケーションをテストするためのノウハウをメモとして残したい
いろいろ検討中†
まだちゃんとしたテストは書き始めたばかりなので全然やり方が定まってません。 このレベルの物をこんなところに晒すのも何なのですが、とりあえずメモとして書いちゃいます。
コントローラのテスト†
途中ロケールの処理が入っていますが、そのあたりの構成は ソフトウェア/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
独自拡張を読み込む†
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 の翻訳の抜けを検出する†
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†
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
コメント†
Counter: 8956 (from 2010/06/03),
today: 2,
yesterday: 2