ActiveSupport_TestCase + shoulda によるテスト の変更点
更新- 追加された行はこの色です。
- 削除された行はこの色です。
- ソフトウェア/rails/ActiveSupport_TestCase + shoulda によるテスト へ行く。
- ソフトウェア/rails/ActiveSupport_TestCase + shoulda によるテスト の差分を削除
[[公開メモ]]
#contents
* ActiveSupport::TestCase + shoulda による Ruby on Rails のテスト [#ge5f5cd1]
- ActiveSupport::TestCase は Rails 標準のテストフレームワーク
- [[shoulda>https://github.com/thoughtbot/shoulda]] は読みやすいテストケースを書くための拡張
これらを使って Rails アプリケーションをテストするためのノウハウをメモとして残したい
ネットで調べると rspec + shoulda-matchers の記事が多くて、
MiniTest + shoulda-context + shoulda-matchers の情報になかなかたどり着けないので。。。
* 環境 [#ab825549]
現在のこちらの環境は以下の通りです。
-rails 4.0.4
-minitest 4.7.5
-shoulda 3.5.0
* いろいろ検討中 [#rc1df26c]
まだちゃんとしたテストは書き始めたばかりなので全然やり方が定まってません。
このレベルの物をこんなところに晒すのも何なのですが、とりあえずメモとして書いちゃいます。
** 便利な gem [#d7228abc]
- development 用
:better_errors+binding_of_caller| エラー画面でコールスタック上の変数値を参照するなどいろいろできるようになる
:tapp| 昔ながらの puts によるデバッグに於いて caller チェーンを切らずにデバッグ出力するのに便利
- test 用
:shoulda|読みやすいテストケースを書けるようにする
:database_rewinder|データベースを高速に初期化する
:tapp|上記参照
Gemfile
LANG:ruby
group :development do
gem 'better_errors'
gem 'binding_of_caller'
gem 'tapp'
end
group :test do
gem 'database_rewinder'
gem 'shoulda'
gem 'tapp'
end
** shoulda の基本 [#w7e40d51]
shoulda では、
- context の setup で操作を行い
- should で検証を行う
- テストの内容説明は context + should で自動生成される
というのが基本になります。
生の ActiveSupport::TestCase とは考え方が異なるので始めは戸惑います。
ActiveSupport::TestCase であれば、
- setup ですべての test に共通の前処理を行い
- 個々の test で操作を行い、さらに assert も行う
- テストの内容説明は test の名前 + assert の記述で行う
となります。
shoulda の場合は「個々の should の中で操作を行わない」
という点が大きな違いなわけです。
無理矢理対応関係を書けば次のようになります。
||~ shoulda |~ assert |
|~操作 | context #1| setup|
|~| context #2|~|
|~| context #3| test|
|~検証 | should| assert|
下で言えば、
LANG:ruby
[:ja,:en].each do |locale|
context "with locale = #{locale}," do
setup {
# ロケール設定
}
context "with auto-login," do
setup {
# オートログイン準備
}
context 'GET #home' do
setup {
get :home # 実際の操作
}
should 期待される結果1
should 期待される結果2
end
end
end
などとなっています。
これで、
- with locale ja, with auto-login, GET #home should 期待される結果1
- with locale ja, with auto-login, GET #home should 期待される結果2
- with locale en, with auto-login, GET #home should 期待される結果1
- with locale en, with auto-login, GET #home should 期待される結果2
のようにすべてのテストに適切な説明が付きます。
エラーメッセージの例:
LANG:console
...
1) Failure:
AccountControllerTest#test_: with locale en, without login, valid POST #create
should send email to registered address.
[/home/osamu/MyApp/test/controllers/account_controller_test.rb:141]:
Expected true to be nil or false
...
should の中に get :home を書いてしまうと、
should にうまく名前を付けられなくて困ることになります。
- context の setup は、中に含まれるすべての should に対して毎回呼ばれます。
- すべての should に先駆けて一回だけ行いたいような初期化処理がもしあれば、
テストケースの宣言部分(クラススコープ)で行えば良いのだと思います。
ただ、個々のテストは独立していないといろいろややこしいので、
そういう初期化が必要になることはあまり無いんじゃないかとも思います。
** コントローラのテスト [#m9c4f2cd]
途中ロケールの処理が入っていますが、そのあたりの構成は
[[ソフトウェア/rails/国際化]] に書いた通りです。
test/controllers/account_controller_test.rb
LANG:ruby(linenumber)
#coding: utf-8
require 'test_helper'
require_relative '../models/_test_params.rb'
class AccountControllerTest < ActionController::TestCase
tests AccountController # この例のように標準的な命名をしている限り、
# コントローラ名の指定は省略してもよい
# Params::param_name の形で Hash 形式のモデル作成用
# パラメータを参照可能にする
# Params::param_name(key1: value1, key2: value2) として
# いくつかのキーをオーバーライドすることも可能
include TestParams
# setup とテストケース should の中とで情報をやりとりするのに
# 普通にインスタンス変数 @xxx を使うか、反則技でクラス変数 @@xxx を
# 使うか迷い中
#
# インスタンス変数を使うのが普通だが、
# - クラススコープからインスタンス変数にアクセスできなくて
# 残念に感じることがたまにある
# - 比較的多数のインスタンス変数がフレームワークによって
# 使われているため、命名の衝突が怖い
#
# クラス変数を使うのはかなり異端
# - 本来の目的からずれまくってるので完全に反則技
# - 初期化し忘れると直前のテストケースの値が残ってしまうのも問題
# - その代わり、どこからでもアクセスできるのがかなり便利?
# ActionController::TestCase にて、以下のクラス変数が使われている。
#
# @@required_fixture_classes: false
# @@already_loaded_fixtures: {}
# @@test_suites: {ActiveSupport::TestCase=>true, ...}
# @@current: #<AccountControllerTest:0xb192174>
#
# クラス変数を使う場合は、これらおよび未来の TestCase のクラス変数と
# 名前が被らないよう注意する必要あり
# インスタンス変数として以下が定義されるので、名前が
# 被らないように注意が必要
#
# インスタンス変数 利用時の記法 内容
# @__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 内で定義されたインスタンス変数
# すべての should に先駆けて1回だけ行いたい処理がもしもあれば
# ここに書けばいい(通常あまり必要なさそう?)
# 初期化処理1
# 初期化処理2
# すべての context に共通の初期化処理
# これは個々の should の直前に、毎回呼ばれることになる
setup {
clear_all
}
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 #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 "GET #new" do
setup {
get :new
}
should respond_with :success
should render_template :new
should_not set_the_flash
end
context 'invalid POST #create' do
setup {
post :create, account: Params::invalid_account
}
should "not create account" do
assert_equal 0, Account.count
end
should render_template :new
end
context 'valid POST #create' do
setup {
post :create, account: Params::valid_account
}
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 Params::valid_account[:email], email_sent.to
end
end
end
context "with auto-login," do
setup {
@@account = Account.create!(
Params::valid_account(
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
should render_template :edit
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! Params::valid_registration
}
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
* モデル生成用パラメータ [#c095e6b7]
test/models/_test_params.rb
LANG:ruby(linenumber)
# coding: utf-8
module TestParams
module Params
#
# name で指定される名前のクラス関数を作成する
# Params::param_name = ハッシュを返す
# Params::param_name(key: value) = ハッシュに hash[:key]=value してから返す
#
def self.param(param_name, hash)
class_eval <<-EOS
def self.#{param_name}(options={})
#{hash.inspect}.merge(options)
end
EOS
end
param :valid_account, {
name: "Foo Baa",
email: "account@example.com",
affiliation: "My institute",
address: "My address",
telephone: "+00-000-0000"
}
param :invalid_account, {
name: ''
}
param :valid_registration, {
gender: 1,
membership: 1,
student: false,
receipt_address: 'My address',
payaccount: 'My Account'
}
param :invalid_registration, {
gender: 1,
membership: 1,
student: false,
receipt_address: nil,
payaccount: nil
}
end
end
* 独自拡張を読み込む [#xdb96442]
test/test_helper.rb
LANG:ruby
...
# I18n.translate の翻訳の抜けを検出する
require 'test_i18n_translation_strictly'
# shoulda 用のマクロを読み込む
Dir[File.expand_path('../shoulda_macros', __FILE__) << '/*.rb'].each do |file|
require file
end
...
** I18n.translate の翻訳の抜けを検出する [#ub2cc8f0]
test/test_i18n_translation_strictly.rb
LANG:ruby(linenumber)
# coding: utf-8
# 翻訳が見付からないときに例外を投げる
# 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
** should redirect_to_path と set_the_flash.to の拡張 [#r6fcffc4]
test/shoulda_macros/action_controller_macros.rb
LANG:ruby(linenumber)
module Shoulda::Matchers::ActionController # :nodoc:
# 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
# set_session.to と同様に、
# set_the_flash.to{ some_code } の形の呼び出しを可能にする
# controller 側の I18n.locale を使ってメッセージを評価するには
# set_the_flash.to{ I18n.t :message_id } の形での呼び出しにが必須になる
class SetTheFlashMatcher
alias :to_without_block :to
def to(value=//, &block)
@value_block = block
to_without_block(value)
end
alias :matches_q_without_block :"matches?"
def matches?(controller)
if @value_block
to_without_block( @context.instance_eval(&@value_block) )
end
matches_q_without_block(controller)
end
def in_context(context)
@context = context
self
end
end
end
*** context_for_all_locales [#r1d0c88f]
というのも作っておくと良さそう
** エラーの生じた should を特定しやすくする [#wc579ebc]
shoulda からのエラーメッセージが以下のように shoulda
ライブラリや shoulda macro の中を指してしまうことがあります。
LANG:console
1) Failure:
AccountControllerTest#test_: with locale en, without login, invalid POST #create
should set the flash.
[/var/lib/gems/2.0.0/gems/shoulda-context-1.2.1/lib/shoulda/context/context.rb:344]:
Expected the flash to be set, but no flash was set
このようなメッセージからエラーが生じたテストの場所を特定するのはなかなか手間がかかります。
ごたごたは飛ばして解決策へ飛ぶ場合は [[こちら>#s29326e1]]
*** 試行錯誤 [#c418a6e5]
で、例外のバックトレースをいじって改善できない物か???と考えたわけです。
ActiveSupport::TestCase ~
https://github.com/rails/rails/blob/master/activesupport/lib/active_support/test_case.rb
gem 'minitest' を取り込んで ::Minitest::Test を継承している。ruby 標準の minitest ではないらしい?
これかな?~
https://github.com/seattlerb/minitest
capture_exceptions で self.failures にエラーを溜めて、~
https://github.com/seattlerb/minitest/blob/d5d43cef9a3fd4a0eea972dde125ed5ba1ddb821/lib/minitest/test.rb#L202
to_s で文字に直す?~
https://github.com/seattlerb/minitest/blob/d5d43cef9a3fd4a0eea972dde125ed5ba1ddb821/lib/minitest/test.rb#L261
場所を表わす部分は location で、self.failure というのがどこから来るのか分からないものの、
アサート例外の location から得ているように見える~
https://github.com/seattlerb/minitest/blob/d5d43cef9a3fd4a0eea972dde125ed5ba1ddb821/lib/minitest/test.rb#L224
failure は Runnable で定義されていた → 何のことはない failures.first のことだった~
https://github.com/seattlerb/minitest/blob/d5d43cef9a3fd4a0eea972dde125ed5ba1ddb821/lib/minitest.rb#L345
Minitest::Assertion 例外の location はここ~
https://github.com/seattlerb/minitest/blob/d5d43cef9a3fd4a0eea972dde125ed5ba1ddb821/lib/minitest.rb#L633
LANG:ruby
module Minitest
VERSION = "5.3.4" # :nodoc:
...
class Assertion < Exception
...
def location
last_before_assertion = ""
self.backtrace.reverse_each do |s|
break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/
last_before_assertion = s
end
last_before_assertion.sub(/:in .*$/, "")
end
この backtrace を遡っている部分の条件を変更してやれば良い・・・のかと思ったら違うみたいです???
どこかで間違った???
ああ、動かしてるバージョンが段違いでした orz~
上記 seattlerb/minitest でメンテしているのは 5.x.x 系列で、4.x.x 系列は 4.7.2 あたりが最後?
古いコードだと、普通に rescue で捕まえて runner.puke してますね~
https://github.com/seattlerb/minitest/blob/7e25922c340fa1cfdbeffaf8f37ec020cf6a4fff/lib/minitest/unit.rb#L1255
puke は location を呼んでるから、~
https://github.com/seattlerb/minitest/blob/7e25922c340fa1cfdbeffaf8f37ec020cf6a4fff/lib/minitest/unit.rb#L954
この location を書き換えればいいのかと思ったのですが・・・
LANG:ruby
module Minitest
class Unit
def location e
e.backtract.join("\n")
end
end
end
として得られたトレースはこんな感じ。
LANG:console
minitest-4.7.5/lib/minitest/unit.rb:195:in `assert'
shoulda-context-1.2.1/lib/shoulda/context/assertions.rb:91:in `safe_assert_block'
shoulda-context-1.2.1/lib/shoulda/context/assertions.rb:83:in `assert_rejects'
shoulda-context-1.2.1/lib/shoulda/context/context.rb:358:in `block in should_not'
shoulda-context-1.2.1/lib/shoulda/context/context.rb:413:in `instance_exec'
shoulda-context-1.2.1/lib/shoulda/context/context.rb:413:in `block in create_test_from_should_hash'
minitest-4.7.5/lib/minitest/unit.rb:1258:in `run'
minitest-4.7.5/lib/minitest/unit.rb:933:in `block in _run_suite'
minitest-4.7.5/lib/minitest/unit.rb:926:in `map'
minitest-4.7.5/lib/minitest/unit.rb:926:in `_run_suite'
minitest-4.7.5/lib/minitest/parallel_each.rb:71:in `block in _run_suites'
minitest-4.7.5/lib/minitest/parallel_each.rb:71:in `map'
minitest-4.7.5/lib/minitest/parallel_each.rb:71:in `_run_suites'
minitest-4.7.5/lib/minitest/unit.rb:877:in `_run_anything'
minitest-4.7.5/lib/minitest/unit.rb:1085:in `run_tests'
minitest-4.7.5/lib/minitest/unit.rb:1072:in `block in _run'
minitest-4.7.5/lib/minitest/unit.rb:1071:in `each'
minitest-4.7.5/lib/minitest/unit.rb:1071:in `_run'
minitest-4.7.5/lib/minitest/unit.rb:1059:in `run'
minitest-4.7.5/lib/minitest/unit.rb:795:in `block in autorun'
バックトレースに TestCase の行が含まれていない???
https://github.com/thoughtbot/shoulda-context/blob/5305f310d432992e2a1e74f542c62104272edacf/lib/shoulda/context/context.rb#L356
LANG:ruby
def should_not(matcher)
name = matcher.description
blk = lambda { assert_rejects matcher, subject }
self.shoulds << { :name => "not #{name}", :block => blk }
end
この lambda で作った blk が shoulds に溜められて、後から順に実行され、そこでエラーが出ると。
これだと例外から行番号を得るのは難しいですね。
*** 解決 [#s29326e1]
むしろ、この :name に行番号を突っ込んじゃえばいいという話かも。
test/shoulda_macros/add_caller_information.rb
LANG:ruby
#coding: utf-8
module Shoulda
module Context
class Context # :nodoc:
# should ブロックの :name に行番号を書き足す
def add_caller_line_number(should)
# 現バージョンでは2番目が呼び出し元になるみたい
caller(2).first =~ /:(\d+):in /
should[:name] = should[:name].to_s + " (at line #{$1})"
end
alias :should_without_caller_information :should
def should(name_or_matcher, options = {}, &blk)
count_before = self.shoulds.count
should_without_caller_information(name_or_matcher, options = {}, &blk)
if self.shoulds.count > count_before
add_caller_line_number(self.shoulds.last)
else
add_caller_line_number(self.should_eventuallys.last)
end
end
alias :should_not_without_caller_information :should_not
def should_not(matcher)
should_not_without_caller_information(matcher)
add_caller_line_number(self.shoulds.last)
end
alias :should_eventually_without_caller_information :should_eventually
def should_eventually(name, &blk)
should_eventually_without_caller_information(name, &blk)
add_caller_line_number(self.should_eventuallys.last)
end
end
end
end
これで、
LANG:console
3) Failure:
AccountControllerTest#test_: with locale ja, without login, GET #home should not
set the flash (at line 35).
[/var/lib/gems/2.0.0/gems/shoulda-context-1.2.1/lib/shoulda/context/context.rb:358]:
Did not expect the flash to be set, but was #<ActionDispatch::Flash::FlashHash:0xad041fc ...
などというように、should や should_not に対応する行番号が (at line xxx) の形で付くようになりました。
caller() は遅いので、called_from を作った、という記事がありました。テストの規模が大きい場合には検討の価値があるかもしれません。(このライブラリ、Ruby 1.8 以外に対応していない?)~
http://magazine.rubyist.net/?0031-BackTrace#l7 ~
とはいえ1万個の should を作ってようやく1秒違うかどうかなので、気にするだけ損なのかも。
** minitest-focus の代わりに shoulda-focus [#g183283d]
Minitest::Unit によるテストを便利にするための gem として minitest-focus というものがあります。
http://docs.seattlerb.org/minitest-focus/
これは、 編集中のテストケースの中で、ある特定のテストだけを実行したいときに そのテストに focus というキーワードを付けることで、同じテストケースの中の他のテストをすべてスキップできる、という便利な gem です。
特に guard-minitest でテストを自動実行しているときには非常に便利です。
ただ、shoulda-context との組み合わせはどうもよくありません。
そこで、shoulda-context を併用する際に使える focus を以下のように作って使っています。
この記事は Quiita にも載せました。http://qiita.com/osamu_takeuchi/items/256e044a3e802a5abff0
test/shoulda_macros/shoulda-focus.rb
LANG:ruby(linenumber)
# shoulda-focus.rb
#
# tested with: shoulda (3.5.0), minitest (4.7.5)
#
# Marking a context or a should by 'focus'
# allows testing only the focused tests,
# skipping all other unfocused ones in a TestCase.
#
# REVISIONS:
# 2014-05-30 report number of skipped tests in the result
#
# EXAMPLE:
#
# class MyTest < ActionController::TestCase
#
# should "will not be tested" do assert false end
#
# context "Unfocused context" do
# should "will not be tested" do assert false end
#
# focus # <<<<<< FOCUSED!
# should "will be tested" do assert true end
#
# should "will not be tested" do assert false end
# end
#
# focus # <<<<<< FOCUSED!
# context "Focused context" do
# should "will be tested" do assert true end
# should "will be tested" do assert true end
# should "will be tested" do assert true end
# context "Focused sub context" do
# should "will be tested" do assert true end
# end
# end
#
# end
#
module Shoulda
module Context
class << self
attr_accessor :focused # indicates we are in a focused context
attr_writer :focus_next # indicates focus just called, we will focus the next context/should
attr_accessor :focus_used # indicates focus is used, then we will skip unfocused shoulds
# to see if we should focus the next context
def to_focus?
# either we are in a focused context or 'focus' was just called
result = @focused || @focus_next
@focus_next = false
result
end
end
module ClassMethods
# call this method to mark the next context/should to be focused
def focus
unless Shoulda::Context.focus_used
puts "",
" * FOCUS CALLED FOR [#{self.name}] WE WILL SKIP UNFOCUSED TESTS",
" " + caller(2).first.sub(/:in .*/, ''),
""
end
Shoulda::Context.focus_used = true
Shoulda::Context.focus_next = true
end
end
class Context
alias :initialize_without_focus :initialize
def initialize(name, parent, &blk)
was_focused = Shoulda::Context.focused
Shoulda::Context.focused = Shoulda::Context.to_focus?
initialize_without_focus(name, parent, &blk)
Shoulda::Context.focused = was_focused
end
alias :should_without_focus :should
def should(name_or_matcher, options = {}, &blk)
to_focus = Shoulda::Context.to_focus?
count_before = shoulds.count
should_without_focus(name_or_matcher, options, &blk)
if shoulds.count > count_before
shoulds.last[:focused] = to_focus
else
should_eventuallys.last[:focused] = to_focus
end
end
alias :should_not_without_focus :should_not
def should_not(matcher)
to_focus = Shoulda::Context.to_focus?
should_not_without_focus(matcher)
shoulds.last[:focused] = to_focus
end
alias :create_test_from_should_hash_without_focus :create_test_from_should_hash
def create_test_from_should_hash(should)
if not Shoulda::Context.focus_used or should[:focused]
create_test_from_should_hash_without_focus(should)
else
else
# to report skipped tests verbosely, use skip() instead (will be too noisy and slow)
# should[:block] = Proc.new(){ skip() }
should[:block] = Proc.new(){ increase_skip_count_quietly() }
create_test_from_should_hash_without_focus(should)
end
end
end
end
end
MiniTest::Unit::TestCase.class_eval do
alias :run_without_quiet_skip :run
def run(runner)
@runner = runner
run_without_quiet_skip(runner)
end
def increase_skip_count_quietly()
@runner.instance_eval do
@skips += 1
end
end
end
* assert_XXX [#h14dab47]
|>|>|>|>|~[[mt>https://github.com/seattlerb/minitest/blob/7e25922c340fa1cfdbeffaf8f37ec020cf6a4fff/lib/minitest/unit.rb#L190]] [[AS>https://github.com/rails/rails/blob/4-0-stable/activesupport/lib/active_support/testing/assertions.rb]] [[AS dep>https://github.com/rails/rails/blob/4-1-stable/activesupport/lib/active_support/testing/deprecation.rb]] [[AD>https://github.com/rails/rails/tree/4-0-stable/actionpack/lib/action_dispatch/testing/assertions]] [[AC>https://github.com/rails/rails/blob/4-0-stable/actionpack/lib/action_controller/test_case.rb#L95]] [[sd>https://github.com/thoughtbot/shoulda-context/blob/master/lib/shoulda/context/assertions.rb]] [[jr>https://github.com/rails/jquery-rails/blob/master/lib/jquery/assert_select.rb]] で実装される assert_xxxx |
|名前|引数|テスト内容|refuse|実装|
| assert |test, msg = nil | test | ○ |mt|
| assert_accepts | matcher, target, options = {} | matcher.matches?(target) | | sd |
| assert_blank | obj, msg=nil | obj.blank? | | AS |
| assert_contains | collection, x, extra_msg = "" | x can be regexp | | sd |
| assert_deprecated | match = nil, &block | | | AS |
| assert_difference|expression, diff = 1, msg = nil, &block|block 前後で変化||AS|
| assert_does_not_contain | collection, x, extra_msg = "" | x can be regexp | | sd |
| assert_dom_equal | expected, actual, message = "" | HTML::Document.new(?).rootを比較 | | AD |
| assert_dom_not_equal | expected, actual, message = "" | HTML::Document.new(?).rootを比較 | | AD |
| assert_empty |obj, msg = nil | obj.empty? | ○ |mt|
| assert_equal |exp, act, msg = nil | exp == act | ○ |mt|
| assert_generates | expected_path, options, defaults={}, extras = {}, message=nil | | | AD |
| assert_in_delta |exp, act, delta = 0.001, msg = nil | (act-exp).abs <= delta | ○ |mt|
| assert_in_epsilon |a, b, epsilon = 0.001, msg = nil | 上の別名 | ○ |mt|
| assert_includes |collection, obj, msg = nil | collection.include?(obj) | ○ |mt|
| assert_instance_of |cls, obj, msg = nil | obj.instance_of?(cls) | ○ |mt|
| assert_kind_of |cls, obj, msg = nil | obj.kind_of?(cls) | ○ |mt|
| assert_match |matcher, obj, msg = nil | matcher =~ obj | ○ |mt|
| assert_nil |obj, msg = nil | obj.nil? | ○ |mt|
| assert_no_difference|expression, msg = nil, &block|block 前後で保存||AS|
| assert_no_match |matcher, obj, msg = nil | matcher !~ obj | |mt|
| assert_no_tag | *opts | html_document.find(*opts) | | AD |
| assert_not |obj, msg = nil|!obj||AS|
| assert_not_deprecated | &block | | |AS|
| assert_not_empty |obj, msg = nil | !obj.empty? | |mt|
| assert_not_equal |exp, act, msg = nil | exp != act | |mt|
| assert_not_in_delta |exp, act, delta = 0.001, msg = nil | (act-exp).abs > delta | |mt|
| assert_not_in_epsilon |a, b, epsilon = 0.001, msg = nil | 上の別名 | |mt|
| assert_not_includes |collection, obj, msg = nil | !collection.include?(obj) | |mt|
| assert_not_instance_of |cls, obj, msg = nil | !obj.instance_of?(cls) | |mt|
| assert_not_kind_of |cls, obj, msg = nil | !obj.kind_of?(cls) | |mt|
| assert_not_nil |obj, msg = nil | !obj.nil? | |mt|
| assert_not_operator |o1, op, o2 = UNDEFINED, msg = nil | !o1._send_(op, o2) | |mt|
| assert_not_predicate |o1, op, msg = nil | !o1._send_(op) | |mt|
| assert_not_respond_to |obj, meth, msg = nil | !obj.respond_to?(meth) | |mt|
| assert_not_same |exp, act, msg = nil | !exp.equal?(act) | |mt|
| assert_nothing_raised | | | | |
| assert_operator |o1, op, o2 = UNDEFINED, msg = nil | o1._send_(op, o2) | ○ |mt|
| assert_output |stdout = nil, stderr = nil | | |mt|
| assert_predicate |o1, op, msg = nil | o1._send_(op) | ○ |mt|
| assert_raise | | | | |
| assert_raises |*exp | | |mt|
| assert_recognizes | expected_options, path, extras={}, msg=nil | | | AD |
| assert_redirected_to | options = {}, message=nil | | | AD |
| assert_rejects | matcher, target, options = {} | !matcher.matches?(target) | | sd |
| assert_respond_to |obj, meth, msg = nil | obj.respond_to?(meth) | ○ |mt|
| assert_response | type, message = nil | | | AD |
| assert_routing | path, options, defaults={}, extras={}, message=nil | | | AD |
| assert_same |exp, act, msg = nil | exp.equal?(act) | ○ |mt|
| assert_same_elements | a1, a2, msg = nil | compare array asif it is set | | sd |
| assert_select | *args, &block | | | AD |
| assert_select_email | &block | | | AD |
| assert_select_encoded | element = nil, &block | | | AD |
| assert_select_jquery | *args, &block | | | jr |
| assert_send |[recv, msg, *args], m = nil | recv._send_(msg, *args) | |mt|
| assert_silent | |assert_output "", "" | |mt|
| assert_tag | *opts | html_document.find(*opts) | | AD |
| assert_template | options = {}, message = nil | | | AC |
| assert_throws |sym, msg = nil | | |mt|
- mt: minitest = https://github.com/seattlerb/minitest/blob/7e25922c340fa1cfdbeffaf8f37ec020cf6a4fff/lib/minitest/unit.rb#L190
- AS: ActiveSupport = https://github.com/rails/rails/blob/4-0-stable/activesupport/lib/active_support/testing/assertions.rb- AS: ActiveSupport deplication = https://github.com/rails/rails/blob/4-1-stable/activesupport/lib/active_support/testing/deprecation.rb
- AD: ActionDispatch = https://github.com/rails/rails/tree/4-0-stable/actionpack/lib/action_dispatch/testing/assertions
- AC: ActionController = https://github.com/rails/rails/blob/4-0-stable/actionpack/lib/action_controller/test_case.rb#L95
- sd: Shoulda = https://github.com/thoughtbot/shoulda-context/blob/master/lib/shoulda/context/assertions.rb
- jr: jquery-rails = https://github.com/rails/jquery-rails/blob/master/lib/jquery/assert_select.rb
AD: with_routing ~
AD: css_select ~
* shouda の matcher [#gd8d5b05]
** 一覧 [#m5e4b4ec]
#multicolumns
*** [[ActiveModel Matchers>https://github.com/thoughtbot/shoulda-matchers#activemodel-matchers]] [#n7a5004d]
-allow_mass_assignment_of
-''allow_value''
-ensure_inclusion_of
-ensure_exclusion_of
-ensure_length_of
-have_secure_password
-validate_absence_of
-validate_acceptance_of
-validate_confirmation_of
-validate_numericality_of
-validate_presence_of
-validate_uniqueness_of
#multicolumns
*** [[ActiveRecord Matchers>https://github.com/thoughtbot/shoulda-matchers#activerecord-matchers]] [#x0e4eaa5]
-accept_nested_attributes_for
-belong_to
-have_many
-have_one
-have_and_belong_to_many
-have_db_column
-have_db_index
-have_readonly_attribute
-serialize
#multicolumns
*** [[ActionController Matchers>https://github.com/thoughtbot/shoulda-matchers#actioncontroller-matchers]] [#kdd1429f]
-filter_param
-permit
-''redirect_to''
-''render_template''
-''render_with_layout''
-rescue_from
-''respond_with''
-route
-''set_session''
-''set_the_flash''
-use_after_filter
-use_after_action
-use_around_filter
-use_around_action
-use_before_filter
-use_around_action
#multicolumns(end)
始めからコード側で宣言的に書かれている、例えば has_many なんかを
わざわざテストしても、ほとんどコピペになるだけで意味は薄いと思います。
なので、上記のように matcher はたくさんある物の、頻繁に使うのは、
ActionModel の
-allow_value
-- allow_value('http://foo.com', 'http://bar.com/baz').for(:website_url)
-- allow_value('2013-01-01').for(:birthday_as_string).on(:create)
-- allow_value('open', 'closed').for(:state).with_message('State must be open or closed')
ActionController の
-redirect_to
-- redirect_to :list
-- redirect_to("description") { posts_path }
-render_template
-- render_template 'show'
-- render_template :show
-respond_with
-- respond_with 403
-- respond_with :success
-- respond_with 500..600
-route
-- route(:get, '/posts').to(controller: 'posts', action: 'index')
-- route(:get, '/posts/1').to('posts#show', id: 1)
-set_session
-- set_session :key
-- set_session(:key).to("value")
-- set_session(:key).to{ calc_expectation }
-set_the_flash
-- set_the_flash
-- set_the_flash.to('value')
-- set_the_flash.to(/regexp/)
-- set_the_flash.to{ calc_expectation_in_string_or_regexp } # 上のコードによる拡張
-- set_the_flash[:key]
-- set_the_flash[:key].to('value')
-- set_the_flash[:key].to(/regexp/)
-- set_the_flash[:key].now
-- set_the_flash[:key].to('value').now
くらいかもしれません。
** shoulda-matcher の作り方 [#m38d0482]
shoulda の matcher は matches?, description, failure_message などを実装した
Matcher オブジェクトを作成するファクトリメソッドとして実装されます。
それが分かっていれば上記 redirect_to_path のように、既存の matcher にパラメータを与えて作成して返すことは簡単にできます。
Matcher クラスを1から自分で作る場合のやりかたは、set_session のコードを見れば一通り分かりそうです。
https://github.com/thoughtbot/shoulda-matchers/blob/master/lib/shoulda/matchers/action_controller/set_session_matcher.rb
* guard-minitest で自動実行 [#wb648c09]
[[guard-minitest>https://github.com/guard/guard-minitest]] は、Rails 4 用の guard-test です。
guard により、ソースファイルを編集&保存するたびに関連のテストを自動実行することができます。
Rails 4 では標準のテスト環境が Test::Unit から Minitest::Unit に変更になったため、
guard-test が動かなくなり、代わりに開発されているのが guard-minitest のようです。
- http://bitplaying.tumblr.com/post/55711657471/rails-4-defaults-to-minitest-frustration-follows
LANG:console
$ jed Gemfile
group :development do
gem 'guard-minitest'
end
$ bundle install
$ bundle exec guard init minitest
$ jed Guardfile
# Rails 4 となっている部分を有効にする
$ bundle exec guard
05:47:51 - INFO - Guard is using TerminalTitle to send notifications.
05:47:51 - INFO - Guard::Minitest 2.3.0 is running, with Minitest::Unit 4.7.5!
05:47:51 - INFO - Running: all tests
Run options: --seed 40183
# Running tests:
...
05:47:56 - INFO - Guard is now watching at '(Rails root)'
[1] guard(main)>_
別のコンソールから、
LANG:console
$ touch app/controllers/account_controller.rb
とすると元の窓で、
LANG:console
05:49:07 - INFO - Running: test/controllers/account_controller_test.rb
Run options: --seed 43771
# Running tests:
...
[1] guard(main)>_
ちゃんとテストが自動実行されました。
恐らく、ファイルを編集する窓と、テスト結果を表示する窓とを開いておいて、
保存する度に横目でテスト結果を確認するような使い方になりそうです?
** たしかに自動実行はされるけど [#bf45d0f5]
例えば app/models/account.rb を編集すると test/models/account_test.rb が実行されるだけで、Account モデルを使う app/controllers/account_controller.rb のテストまで自動実行されるわけではありません。
実際のところ、account_controller_test.rb も合わせて実行するように設定することまでなら簡単にできそうですが、ちゃんと依存関係をたどってその他のコントローラや統合テストまで起動するのは恐らく簡単にはできないのだと思います。
ですから、app/models/account.rb の編集が一段落したら、例えば application_controller.rb に touch して全テストを実行する(そのように設定しておいて)といった使い方になるんだと思います。
guard で全テストを実行するには、"guard(main)>" のプロンプトで何も入力せず ENTER キーを押せばよいので、そういう用途にも楽ちんに対応できますね。
** 表示を見やすくする [#gbbca72e]
[[minitest-reporters>https://github.com/kern/minitest-reporters]] を使うと結果をカラー表示にできるようです。
- http://stackoverflow.com/questions/7959523/can-minitest-do-something-like-rspec-color-format-doc
LANG:console
$ jed Gemfile
group :test do
...
gem 'minitest-reporters'
end
$ bundle install
$ jed test/test_helpder.rb
...
require "minitest/reporters"
Minitest::Reporters.use!
...
$ bundle exec guard
ここには載せませんが、確かに出力がカラーになりました。
* should_eventually と skip() [#m30d873b]
shoulda-context の機能に should_eventually というのがあって、
通常 should と書くところを should_eventually と書いておく、
あるいは、should に名前だけ付けて do end を与えないことで、
ビヘイビャーを記述しつつ、実装を後から行うようにできます。
LANG:ruby
should_eventually "be implemented in the future" do # should_eventually
assert false # will not be tested for now
end
should "be implemented in the future, too" # should without body
このときテスト画面には、
LANG:console
* DEFERRED: should be implemented in the future (at line 139).
* DEFERRED: should be implemented in the future, too (at line 143).
というような表示を残すのみで、テストとしてはカウントされません。
一方、MiniTest::Unit::TestCase には skip() という関数が用意されていて、
テスト関数から skip() を呼ぶことで MiniTest::Skip という例外を投げることができて、
そのテスト関数の以降のテストをキャンセルできます。このとき、テストは skipped としてカウントされます。
LANG:ruby
should "be implemented in the future" do
skip() # cancel testing
assert false # this assertion will not be tested for now
end
当分実装しなさそうな、しばらくの間完全に忘れていて良い仕様については
should_eventually または do end なしの should で記述しておけばよく、
逆にペンディングになっている仕様があることを目立たせたければ skip() を使えば良いのだと思います。
* RSpec の方が良いんでしょうか [#m55d50d7]
Web 上で情報を探していると、RSpec の記事の方がたくさん目にとまります。
長いものには巻かれておいた方が良いのかもしれません。
* 役立ちそうな記事 [#x64e134e]
- http://www.railstips.org/blog/archives/2009/02/21/shoulda-looked-at-it-sooner/
- http://d.hatena.ne.jp/language_and_engineering/20091023/p1
- http://www.atmarkit.co.jp/ait/articles/1112/22/news132.html
- http://guides.rubyonrails.org/testing.html
- http://maskana-soft.com/rails/pro/detail/test
* コメント [#yfa709ad]
#article_kcaptcha
Counter: 8910 (from 2010/06/03),
today: 1,
yesterday: 2