Randomly Failing Specs

263 views

Published on

Rails Developers Meetup 2017
https://techplay.jp/event/631431

Published in: Internet
  • Be the first to comment

  • Be the first to like this

Randomly Failing Specs

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

×
Save this presentationTap To Close