Randomly Failing Specs
〜稀に落ちるテストとの戦い方〜
Rails Developers Meetup 2017
2017/12/09(土) TECH PLAY SHIBUYA
自己紹介
● 名前: 正徳 巧
● Twitter: 神速(@sinsoku_listy)
● GitHub: sinsoku (@sinsoku)
● 所属: 株式会社grooves
の開発を担当
@sinsoku_listy
@sinsoku
最近Railsの同人誌を書きました
タイトル: Clean Code for Rails
イベント: 技術書典3
頒布価格: 1,000円
イラスト: Ixy
(可愛いのは表紙・裏表紙だけです)
在庫を持ってきているので、興味ある人はぜひ声かけて!!
稀に落ちるテストとは
基本的に成功するが、CIでテストを実行し続けていると
稀に失敗するテスト。
たいていは「リトライ」すると直る。
原因は分かり辛く、再現させるのが難しい。
リトライで誤魔化し、原因を調査するのは後回しになりがち
そんな稀に落ちるテストとの
戦い方を紹介します
今日話すこと
● テストが落ちる事例の紹介
○ ランダム値を使うテスト
○ 実行順序によって落ちるテスト
○ JavaScriptを使うFeature Spec
● 基本的な戦い方
テストが落ちる事例の紹介
ランダム値を使うテスト
ランダムな数字やFaker、現在日時を扱うテストは気をつけないと
稀に落ちる可能性があります。
特に下記の2つに注意してください。
● expectでランダム値を使用する
● uniquness制約の属性にランダム値を使う
事例を紹介します。
事例1: ソート順を指定した後に削除するテスト
feature "xxx" do
# ランダムな1桁の数字
let(:old_sort_order) { Faker::Number.number(1) }
scenario "xxx" do
# ページを表示し、要素を削除する処理
expect(page).to_not have_content old_sort_order
end
end
画面内の"+1"の文字があり、1/10で失敗
事例2: factory_bot + Faker + ユニーク制約
Fakerの値は意外と被る
irb> require 'faker'
irb> 100.times.map { Faker::Lorem.word }.uniq.size
#=> 67
irb> 1000.times.map { Faker::Internet.user_name }.uniq.size
#=> 960
irb> 10000.times.map { Faker::Internet.email }.uniq.size
#=> 9999
Fakerのuniqueメソッドを使う
引用: https://github.com/stympy/faker/tree/v1.8.5#ensuring-unique-values
factory_botのsequenceを使う
実行順序によって落ちるテスト
事例3: Globalな値を上書きしているテスト
RSpec.describe "new feature", type: :request do
context "on production env" do
before { Rails.env = "production" }
it "not displays" do
get "/new_feature"
expect(response).to have_http_status(:not_found)
end
end
end
production になってしまうと、他のテストで意図しないエラーが
起こる可能性がある
事例3: 修正方法
RSpec.describe "new feature", type: :request do
context "on production env" do
before { allow(Rails.env).to receive(:production?) { true } }
it "not displays" do
get "/new_feature"
expect(response).to have_http_status(:not_found)
end
end
end
代わりにstubを使う。stubは別のテストに影響しない
事例4: RSpecのstub_constの罠
引用: https://github.com/rspec/rspec-mocks/issues/1079
事例4: RSpecのstub_constの罠
引用: https://github.com/rspec/rspec-mocks/issues/1079
Failure/Error: Model.new
NoMethodError:
undefined method `new' for #<Module:0x000000089a8be0>
事例4: RSpecのstub_constの罠
stub_constは指定した定数が未定義の場合、新しいModuleを作
成します。
事例4: 修正方法
module StubConstAutoLoader
def stub_const(constant_name, value, options = {})
constant_name.deconstantize.safe_constantize
super
end
end
RSpec::Mocks::ExampleMethods.prepend StubConstAutoLoader
JavaScriptを使うFeature Spec
...の前に Capybara の基本
Capybara の基本的な動き
RSpec.feature "xxx", type: :feature do
after { DatabaseRewinder.clean }
scenario do
visit "/"
click_link "hello"
expect(page).to have_content "Hello"
end
end
Capybara(rack_test)の仕組み
visit expect clean
app.call(env)
click_link
app.call(env)
簡単ですね
次はJavaScriptを使う場合
JavaScriptの処理がある場合の動き
RSpec.feature "xxx", type: :feature, js: true do
after { DatabaseRewinder.clean }
scenario do
visit "/"
click_link "hello"
expect(page).to have_content "Hello"
end
end
JavaScriptの処理がある場合の動き
visit expect clean
boot
click_link
req
(別プロセス)
GET
click
GET
(別スレッド)
res res
Ajaxを入れます
Turobolinks(Ajax)がある場合の動き
visit expect clean
boot
click_link
req
(別プロセス)
GET
click
(別スレッド)
res res
turbolinks
要素が現れるまで待つ
事例5: 稀に起きるActiveRecord::NotFound
事例5: 稀に起きるActiveRecord::NotFound
RSpec.feature "xxx", type: :feature, js: true do
after { DatabaseRewinder.clean }
scenario do
visit "/" # 初期ページに "Hello" も文言が存在する場合
click_link "hello"
expect(page).to have_content "Hello"
end
end
事例5: 稀に起きるActiveRecord::NotFound
visit expect clean
boot
click_link
req
(別プロセス)
GET
click
(別スレッド)
res res
turbolinks
遷移前のページで
expect が成功する
事例5: Ajaxの後は必ずDOMをチェックする
RSpec.feature "xxx", type: :feature, js: true do
after { DatabaseRewinder.clean }
scenario do
visit "/" # 初期ページにも "Hello" が存在する
click_link "hello"
expect(page).to have_content "Hello#show"
expect(page).to have_content "Hello"
end
end
sleep はできるだけ使わない。テストが遅くなります。
基本的な戦い方
基本的な戦い方
● seed値を指定してテストを実行する
● たくさんテストを実行してみる
● test.logを眺める
● 推測してsleepやprintを入れる
● 再現したら、原因を直す
RSpecのseed値
RSpecのテスト実行順序をランダムにしていた場合、実行順序が
原因になっていることがあります。
seed値を指定すると、同じ実行順序を再現できます。
$ rspec --seed SEED spec/user_spec.rb
たくさんテストを実行してみる
$ for n in {1..20};
do rspec spec/user_spec.rb:100 || break; done
ためしに20回ほど実行してみると再現することがあります。
test.logを眺める
ログファイルを眺めていると、意図しないリクエスト、SQLクエリに
気づくことがあります。
$ tail -f log/test.log
まとめ
● ランダム値は気をつけて使用する
● Globalな値は代入じゃなくてstubを使う
● Capybaraの気持ちを理解する
○ Ajaxの後は必ずDOMをチェックする
sleepとrspec-retryが無くても動くテストを書きましょう!
おまけ: AjaxでDOMの更新が起きない場合
これは Capybara では対応できません。
このケースに対応するため GhostPictures という gem を作成
中です。(すみません、間に合いませんでした)
https://github.com/sinsoku/ghost_pictures

Randomly Failing Specs