rspecに潜む、flaky testを可視化したい ~gem編~

こんにちは、SREチームの程です。 皆さんはどんな夏休みをお過ごしでしたか? 僕は今年は初めてサーフィンに挑戦してきました。 サーフィンというと、はたから見てる限りではただ板の上に立ってよいしょっとしているだけに見えますが、これがいざ自分で実際にやってみると本当に難しく、最終的に「解せぬ」状態になりました。 ともあれ、日頃のストレス発散をするには最適なスポーツだと感じました。ぜひ皆さんも「解せぬ」状態を味わってみてください。

本記事の発端

弊社CI環境においては、コードのpushごとに rails rspecによる自動テストを実施しています。 ところがこの頃、「なぜかわからないけどたまに落ちるテストが存在する」という都市伝説が聞こえてくるようになりました。 いわゆる「flaky(不安定な)テスト」というものです。 そうはいっても、ごくたまにしか発生しないという性質上、どのテストケースが不安定になっているのかを把握できていない状況にありました。 そこで今回は、rspec テストに潜在している自動テストのうち不安定になっているテストケースを特定するために実施検討したことについてまとめます。

検討したツール

Flakyテストの検知ツールとして使えそうなツールを調べたところ、下記4つが候補にあがりました。

① quarantine (gem) github.com

② reportportal (OSS) reportportal.io

③ buildpulse (SaaS) buildpulse.io

④ launchable (SaaS) www.launchableinc.com

これらのうちSaaSの製品③④については、flaky条件の判定ロジックが不明瞭だったことや料金面から、今回の検証からは外すこととし、①quarantine と ②reportportal の2つを試しに使ってみました。 本記事では、手軽に使えそうな ①quarantine の話を書いていきます。

quarantine

機能

リポジトリのREADMEを軽く読んでみると大きく下記3つの機能があるようです。

  • すべてのテストケースの実行結果をデータベースに保存する(保存先:Amazon DynamoDB もしくは Google SpreadSheet)
  • 最初の実行で失敗したものの、リトライして成功したケースはFlakyなテストとして扱い、データベース上でquarantineフラグを付与する。
  • Flakyとして扱われたテストは、以後手動でquarantineフラグを外さない限り、実行がスキップされるようになる。

実行結果を自動ですべてデータベースに保存してくれるのはありがたいですね。 さっそく実際につかってみましょう。

実際につかってみる

とりあえずどんな感覚で使えるものなのかを掴むためにまずはローカルのDocker環境で試しに使ってみます。

設定

READMEに記載のとおり、railsrspec周りの設定を行います。 弊社ではAWSを使用しているので、DynamoDBを実行結果の保存先DBとして設定します。 spec_helper.rbに以下のような設定を記述します。

require 'quarantine'
require 'rspec/retry'

Quarantine::RSpecAdapter.bind

RSpec.configure do |config|
  # Also accepts `:credentials` to override the standard AWS credential chain
  config.quarantine_database = {
      type: :dynamodb,
      region: 'ap-northeast-1'
  }
  # Prevent the list of flaky tests from being polluted by local development and PRs
  config.quarantine_record_tests = ENV["BRANCH"] == "add_quarantine"

  config.around(:each) do |example|
    example.run_with_retry(retry: 2)
  end

さらにDynamoDB上にテーブルも作成しましょう。 こちらもREADMEに記載の通り、quarantine側でコマンドが用意されているので活用します。

bundle exec quarantine_dynamodb -r ap-northeast-1

すると次のようなメッセージが。

Aws::Errors::MissingCredentialsError:
  unable to sign request without credentials set

ふむ、AWSのクレデンシャル周りの設定が必要でしたね。 先述したrspec_helper.rb内でも設定可能なようですがとりあえずさっさと動かしたいので、aws configureでDocker全体に設定してしまいます。 設定後、先のコマンドを再実行すると実行が完了したのでDynamoDBのコンソール画面から確認してみます。

f:id:tei-krmx:20210903113309p:plain
quarantine用のDynamoDBテーブル

無事、テーブルが作成されていますね。テーブル名はspec_helper.rb内で設定すればお好きなテーブル名で作成できるようです。

rspecの実行とまさかの結末

続いて、rspecのテストを走らせてみます。

root@327de7a98a9f:/webapp# BRANCH=add_quarantine bundle exec rspec
NoMethodError:
  undefined method `filter' for []:Array

おや、何かエラーが出ていますね。 調べてみると、rubyのArrayクラスに属するfilterメソッドはruby 2.6から導入されているとのことでした。

stackoverflow.com

弊社の環境では、残念ながら ruby2.4.6 で運用されているためバージョンの要件不足が原因そうでした。 もし使えそうならCI上への導入も考えていたので、できればruby2.4.6で使える方法を考えます。 さてどうしたものかと悩みながらstackoverflowの続きを読むと、Array.selectで書き換えできますよとの記述があったので、力技で書き換えてみました。 再度挑戦してみます。今度はどうでしょう。

root@327de7a98a9f:/webapp# BRANCH=add_quarantine  bundle exec rspec --format progress --tag ~@EtoE
Run options: exclude {:EtoE=>true}
....................................................................................F.........................................................................................................................................................................................pending
...................................................................................................................................F..................................................................................................................................................................................................................................................................................................................................................................................................

どうやらrspecが動き出したようです。 しかし、喜んだのも束の間。しばらくするとまた別のエラーが出現しました。

[quarantine] Database errors:
  Aws::DynamoDB::Errors::ValidationException: 1 validation error detected: 
Value '{test_statuses=[WriteRequest(putRequest=PutRequest(item={full_description=com.amazonaws.dynamodb.v20120810.AttributeValue@9eeab6e1, updated_at=com.amazonaws.dynamodb.v20120810.AttributeValue@f333d4d3, last_status=com.amazonaws.dynamodb.v20120810.AttributeValue@623f141a, location=com.amazonaws.dynamodb.v20120810.AttributeValue@5afb98e5, id=com.amazonaws.dynamodb.v20120810.AttributeValue@415308e3, extra_attributes=com.amazonaws.dynamodb.v20120810.AttributeValue@9a576e29, consecutive_passes=com.amazonaws.dynamodb.v20120810.AttributeValue@c742e5d8}, workloadProfileName=null), deleteRequest=null), WriteRequest(
........
deleteRequest=null)]}'

at 'requestItems' failed to satisfy constraint: Map value must satisfy constraint: [Member must have length less than or equal to 25, Member must have length greater than or equal to 1]

お、おう?!今度はAWSのValidationエラーに引っかかってしまいました。メッセージを読む限り、一度にPutできるItem数を超えてしまっているという趣旨のエラーかと推測されます。quarantineのソースコードを見ると、DynamoDBへのデータ保存にはBatchWriteItem APIを使っているようですが、このAPIに関するAWSのドキュメントにも次のような記述がありました。

A single call to BatchWriteItem can write up to 16 MB of data, which can comprise as many as 25 put or delete requests. Individual items to be written can be as large as 400 KB.

docs.aws.amazon.com

ここを突破するにはおそらくBatchWriteItem呼び出し前に中身をばらして小分けにしてAPIを叩くという実装が必要になりそうです。しかしながらこれ以上深入りするのは非効率になりそうと判断し、本gemの導入は今回は見送ることと致しました。掲げている機能は便利そうだったので今後のアップデートに期待したいです。

reportportalについて

quarantine gem については残念な結果となってしまいましたので、もう1つの選択肢としてあがっていた reportportal についても調査しました。こちらの内容については次回の記事でみなさんにまたご紹介していこうと思います。