Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Randomly Failing Specs

3,984 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

×