こんにちは、 SEQ(Software Engineering in Quality) の nakamu です。 SEQ では、自動テストの運用・改善や基盤開発などを行っています。
現在、 freee にて運用している E2E テスト基盤は、 RSpec + Selenium + Capybara を組み合わせたものとなっています。しかし、この構成には一部課題が存在し、その解決を目指して新たな E2E テストフレームワークへの移行、並びに検討を行っています。
その中の1つの Playwright で、 waitForLoadState('networkidle')
*1 というネットワークのアイドル状態が0.5秒続くまで待機するメソッドが、 E2E テスト基盤に望んでいた機能だったので、それに相当する機能を私たちが運用している E2E テスト基盤にも組み込みました。ここでは、Selenium でのネットワーク待機の実現方法をご紹介します。
注意点
- Chrome DevTools Protocol の API を使うので、Chrome 以外のブラウザでは機能しません。
なぜ waitForLoadState('networkidle')
が必要なのか
E2E テストの特性上、ネットワークの影響によって動作が不安定になる場合があります。特に、同一ページ内で値を更新→反映されていることを確認するケースが顕著で、待機が十分でないと反映前の状態で確認してしまいテストが失敗します。
これまでは安定性を高めるために、「ローディングを意味するアイコンやスピナーが表示されている間は待機する」メソッドを作成し、活用していました。しかし、アイコンやスピナーが出てない状態でも通信している場合があります。この場合では従来のメソッドは適用することができません。そのため、要素の値が期待通りになるまで待機するという方法をテストごとに明示的に記述して対応を行っていました。
waitForLoadState('networkidle')
のようにネットワークを監視する機能を実装することで、各テストに明示的な待機時間を設定する必要がなくなります。したがって、今回この機能を実装しました。ソースコードの例は以下のとおりです。
コード例(Ruby)
Gemfile
gem 'selenium-webdriver' gem 'selenium-devtools'
gem selenium-devtools
をインストールします。
network.rb
module E2ETest module Network class << self def requests @requests ||= [] end def clear_request(request) # タイミングによっては実行タイミングがリクエスト終了->リクエスト開始の順になる可能性があるため0.5秒待つ sleep 0.5 @requests.extract! {|item| item && item['requestId'] == request['requestId'] } end end end end
通信開始時に発生するイベントのパラメータを格納する配列を用意します。この配列の要素数で通信中かどうかを見ます。
clear_request
は通信終了系のイベントが発生した時に、 E2ETest::Network.requests
から requestId が一致するパラメータを削除します。
spec_helper.rb
# (略) RSpec.configure do |config| # (略) config.before(:all) do driver.devtools.network.enable # リクエスト開始イベント driver.devtools.network.on(:request_will_be_sent) do |params| # プロダクトの主要なリソースタイプのリクエストを保存する E2ETest::Network.requests << params if !['Ping', 'SignedExchange', 'Manifest', 'WebSocket', 'CSPViolationReport', 'Other'].include?(params['type']) && params['request']['url'].include?('freee.co.jp') end # キャッシュイベント driver.devtools.network.on(:request_served_from_cache) do |params| E2ETest::Network.clear_request(params) end # レスポンス受信イベント driver.devtools.network.on(:response_received) do |params| E2ETest::Network.clear_request(params) end # リソースロード完了イベント driver.devtools.network.on(:loading_finished) do |params| E2ETest::Network.clear_request(params) end # レスポンス受信失敗イベント driver.devtools.network.on(:loading_failed) do |params| E2ETest::Network.clear_request(params) end driver.devtools.page.enable # ページ遷移イベント driver.devtools.page.on(:frame_navigated) do |_params| # ページ遷移時にリクエストをクリアする E2ETest::Network.requests.clear end end end
以下のイベントを監視します。
-
通信が開始した時に発生します。
プロダクト以外へのリクエストまで含めると不安定になる可能性があるので、プロダクトへのリクエストに絞ります。
リソースタイプも絞りたい場合は、 Network.ResourceType から必要な分を指定してください。
-
- リクエストの応答が受信された時に発生します。
-
- リクエストのロードが完了した時に発生します。
-
- リクエストのロードが失敗した時に発生します。
Network.requestServedFromCache
- リクエストがキャッシュからロードされた時に発生します。
-
ページが遷移された時に発生します。
通信中のリクエストが残ってる時にページ遷移すると、
E2ETest::Network.requests
の要素数に不整合が発生するので、その時点のパラメータを全削除します。
helper.rb
def wait_for_network_idle(idle_time = 0.5) wait = Selenium::WebDriver::Wait.new(timeout: WAIT_TIME) wait.until do wait.until { E2ETest::Network.requests.empty? } sleep idle_time E2ETest::Network.requests.empty? end end
まずネットワークがアイドル状態になるまで待機してから、 idle_time
秒後でもアイドル状態かを見ます。この時点で通信中のリクエストが存在すれば、またアイドル状態になるまで待機…を繰り返します。
これで待機したいタイミングで wait_for_network_idle
を使えばOKです。
導入後の効果
テストが安定するようになった
まだ導入して間もないので今後も注視していく必要はありますが、とあるプロダクトのテストスイートの成功率が目に見えて上がりました🎉
ネットワークの待機は大体 wait_for_network_idle
で良くなった
これまでには、さまざまな条件に応じた複数の待機メソッドが存在していましたが、今回の改善によりそれらの多くを削除できるようになりました。これによりテストの安定性が向上し、かつテストコードを書きやすくなりました。
さいごに
今回は Chrome のみの対応ですが、現在策定中のブラウザ自動化プロトコル WebDriver BiDi を各テストフレームワークが対応すれば、他ブラウザでも低レイヤのAPIを使うことができるので、楽しみにその日を待ちたいと思います。
それでは、よい品質を〜