以前社内で夜間のCIが停止する現象に遭遇しました。
当時Seleniumのsleepで描画待ちを5秒で設定していたのですが、日中帯だと1、2秒で描画されるものでも、夜間だと通信帯域が細くなり描画に7秒かかっていたのです。
確かに一律Thread.sleep(10000);と記載してもいいのですが、全てのスリープを夜間用に最大時間にするとテストがものすごい時間になります。
この時、「webdriverは逐次sleepしなきゃいけないからいまいち」と言われたんですが、設定値を与えて都度待たせる方法もあるので、当時「別に逐次sleepしても良いと思うけどなー」と思ったのを覚えています。
ここではSelenium webdriver Javaで適宜行える方法を記載しておきます。
出現までの最大時間を設定する
sleep 自体が悪というわけではありませんが「とりあえず5秒」「念のため10秒」みたいな固定待機を積み重ねると、テストコードが運任せのゲームになっていきます。
たとえば画面描画が1秒で終わる日もあれば、通信状況で8秒かかる日もあります。そして最大値で固定すると全体のテスト時間が長くなり、長期運用をしていくとテストにうんざりします。
固定待機はその差を吸収できないので、
- 速い日は無駄に待つ
- 遅い日は普通に失敗する
という、効率も安定性も微妙な状態になりがちです。
なので Selenium では、「時間」を待つのではなく「状態」を待つようにしておくと、テストが安定すると同時に、運用が始まってからのイライラが起こりにくくなります。
WebDriverWaitを利用すれば要素出現までの最大秒数まで待機させておくことが可能なので、これを利用してアイテム毎に待機メソッド作成しておくだけです。
WebDriverWaitの第二引数はDuration。
import java.time.Duration;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
public static void waitNameText(WebDriver driver, String elementName, String textData) {
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement element = wait.until(
ExpectedConditions.elementToBeClickable(By.name(elementName))
);
element.sendKeys(textData);
}
これを要素ごとにメソッド化しておけば、
- 「表示されるまで待つ」
- 「クリック可能になるまで待つ」
- 「テキストが反映されるまで待つ」
みたいな待機理由をコードに埋め込めます。
テストコードを長く運用すると、処理内容より「待機との戦い」になる現場もあるでしょう。雑な待機は、最初は楽でも、あとから全員を苦しめます。
逆に、待機条件を部品化しておくと、画面変更や通信速度の変化にも耐えやすくなります。
ついでに、クリック対象が画面外にいて失敗するケースも地味によくあるので、画面内に収める方法も考慮しておくと安定します。
ここでがスクロールして待つだけの実装を記載しておきます。必要において処理を追加してください。
import java.time.Duration;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
public static void scrollLinkText(WebDriver driver, String text) {
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement element = wait.until(
ExpectedConditions.presenceOfElementLocated(By.linkText(text))
);
((JavascriptExecutor) driver).executeScript(
"arguments[0].scrollIntoView({block: 'center'});",
element
);
wait.until(
ExpectedConditions.elementToBeClickable(element)
);
}
当たり前ですが、現場によって状況が変わるのでsleepを完全禁止にする必要はありません。
アニメーション終了待ちや、外部制御できないUIなど、「状態検知より固定待機のほうが安全」な場面も普通にあります。
ただ、全部を固定秒数で押し切ろうとすると、最終的に「CIだけが不定期で落ちる」「たまに失敗する運ゲー」が始まります。
重要なのは、「何秒待つか」ではなく「何を待っているのか」をコードに書くことなんだと思います。
現場で起きる失敗
固定sleepを使っている現場は意外と多いと思いますが、ローカルでは成功するのにCIでは失敗するケースがちょこちょこあります。
最初は動いていても、画面変更・通信速度・CI環境の差異で徐々に崩れ始めます。
原因は、多くの場合ネットワークの帯域の問題で、画面描画やAjax通信の完了タイミングが毎回同じではない事とかち合ってしまいます。
単に3秒待つのではなく、
- 「入力欄がクリック可能になるまで待つ」
- 「リンクが表示されるまで待つ」
- 「ボタンが押せる状態になるまで待つ」
という形で、待機理由をコード内に残すほうが運用的な観点からみると安定します。
また、waitとWebDriverWaitを混在させると、待機時間が読みづらくなる場合があります。基本的に、状態を明確に待ちたい箇所はWebDriverWaitに寄せておくと、CIでの失敗原因を追いやすくなります。
参考
公式ドキュメントを読むと大体のことは読み解けると思います。
https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/support/ui/WebDriverWait.html