😴

WEBアプリのE2Eテストが不安定(flaky)かを確かめる

2024/07/10に公開

背景

E2Eテストは、flakyになりがちですが、masterブランチにマージ後にflakyであることに気が付くことがよくあります。テストを書き上げた時点で気がつけることはできないだろうか?

また、CIでflakyなのに手元だと発生しないことがよくあります。何も考えずに簡単に再現させることはできないだろうか?という問題意識から着想を得ました。

概要

経験上、flakyなE2Eテストは待機ステップが不足していることが多く、偶然うまくいっている場合がほとんどです。このパターンを検出するための方法について述べます。

この方法を使用してテストを複数回実行することで、待機ステップ不足によるflakyテストを効果的に検出できます。

Railsでの実装例

rack middlewareを使用した実装例です。
後述する実装では、エンドポイントごとにsleepする時間がランダムで変わるようになっています。感覚的ですが、数十回くらい実行すれば、flakyではないと言えるでしょう。

使い方は、UNSTABLE=1 をセットしてE2Eテストを実行すると、本機能が有効になります。
また、 UNSTABLE _SEED=xxx にseedを指定することで、エンドポイントごとに同じ時間sleepします。

# config/environments/test.rb
[...]
  if((unstable_seed = ENV['UNSTABLE_SEED'].presence) || ENV['UNSTABLE']).present?
    seed = unstable_seed || SecureRandom.hex(10)
    require 'random_sleep_middleware'
    config.middleware.use(
      RandomSleepMiddleware,
      seed: seed,
      ignore_paths: ["/assets/", "/favicon.ico", "/robots.txt"],
      sleep_range: 0..2
    )
  end
[...]
class RandomSleepMiddleware
  def initialize(app, args = {})
    @app = app
    @seed = args[:seed]
    @ignore_paths = args[:ignore_paths] || []
    @sleep_range = args[:sleep_range] || (0..2)

    Rails.logger.info "[RandomSleepMiddleware] Unstable mode enabled with seed: #{@seed}"
  end

  def call(env)
    return @app.call(env) if @ignore_paths.any? { |path| env['PATH_INFO'].start_with?(path) }

    path_seed = Digest::MD5.hexdigest("#{@seed}#{env['PATH_INFO']}").to_i(16)
    sleep_time = Random.new(path_seed).rand(@sleep_range)
    Rails.logger.info "[RandomSleepMiddleware] #{env['PATH_INFO']} sleep #{sleep_time} seconds"
    sleep(sleep_time)

    @app.call(env)
  end
end

以上。

Discussion