時間の操作
Timecopを使って止めた時間の中でテストするのが良い.
Railsには ActiveSupport::Testing::TimeHelpersでも似たことができるが、ネストしてると期待通り動かないとか細かい点でTimecopの方がおすすめです.
ミリ秒を丸める
DB等でミリ秒が保持できない場合もあるのでテストが失敗することがある.
事前にミリ秒を丸めて扱っていた方が良い.
# Good
let(:now) { Time.current }
# Best
let(:now) { Time.current.change(usec: 0) }
Timecopの場合
# Bad
Timecop.freeze(Time.current)
# Best
Timecop.freeze
Timecop.freeze(Time.current.change(usec: 0))
⏳Timeout
RSpecを書いていると思いのほか時間がかかる処理が紛れ込んでしまう.
中には何かしらのタイミングで異常に時間がかかる処理もあったりする.
タイムアウトする様にすることで不安定なテストを特定することはできます.
ただし不安定なテストを改善するわけでじゃないので、無闇にタイムアウトの時間を伸ばすのはよくない.
後、実行環境が非力だとタイムアウトが多発するのにも注意が必要です.
require 'timeout'
it do
# 5秒経過すると `Timeout::Error` がraiseされる
Timeout::timeout(5) do
expect{ subject.execute }.to_not raise_error
end
end
メタデータで制御
テストが少ないうちは大して気にならないが、ある程度の数になるとCIの実行時間が問題になってくる.
そこでRSpecにタイムアウトを仕掛けられる様にした.
spec/support/timeout.rb
に以下のコードを配置する
timeout.rb のコード
require 'timeout'
class RSpecTimeoutError < Timeout::Error
def message
'execution expired by RSpec'
end
end
RSpec.configure do |config|
rspec_timeout = ENV.fetch('RSPEC_TIMEOUT', 0).to_i
config.around(:example) do |example|
sec = example.metadata[:timeout] || rspec_timeout
next example.run if sec <= 0
Timeout::timeout(sec, RSpecTimeoutError) do
example.run
end
end
config.backtrace_exclusion_patterns << /#{Regexp.escape(Pathname.new(File.expand_path(__FILE__)).relative_path_from(Rails.root).to_s)}/
end
使い方
タイムアウトの指定は2種類あり、それぞれ値にはタイムアウトする秒を指定します.
ただし0
を指定した時はタイムアウトは無効になります.
環境変数を指定
RSPEC_TIMEOUT
を指定する事でタイムアウトが有効になります.
ローカル等の通常の環境ではタイムアウトはせず実行してもらいたいためです.
CI等の全てのテストを実行するような場合のみ環境変数を指定すると良いでしょう.
RSPEC_TIMEOUT=20 rspec
メタデータを指定
特定の箇所だけタイムアウトを制御したいときはメタデータを使います.
it('foo', timeout: 30) { ... }