問題
おそらく直面している問題は、メソッドが正しい (そして有効な!) 要素を返すことですが、1 秒後にそれにアクセスしようとすると、古くなってスローされます。
これは通常、次の場合に発生します。
- 新しいページを非同期にロードするか、少なくとも変更する何かをクリックします。
- すぐに (ページの読み込みが完了する前に) 要素を検索すると、それが見つかります!
- ページが最終的にアンロードされ、新しいページがロードされます。
- 以前に見つかった要素にアクセスしようとしましたが、新しいページにも含まれているにもかかわらず、古い要素になっています。
ソリューション
私が知っているそれを解決する4つの方法があります:
適切な待機を使用する
非同期ページに直面している場合は、予想されるすべてのページ読み込み後に適切な待機を使用します。最初のクリックの後に明示的な待機を挿入し、新しいページ/新しいコンテンツが読み込まれるのを待ちます。その後、必要な要素を検索することができます。これは、最初に行う必要があります。これにより、テストの堅牢性が大幅に向上します。
あなたがそれをした方法
私はあなたの方法の変種を2年間使用してきました(ソリューション1の上記の手法と一緒に)、ほとんどの場合完全に機能し、奇妙なWebDriverバグでのみ失敗します. 見つかった要素が見つかった直後 (メソッドから戻る前) に、.isDisplayed()
メソッドなどを介してアクセスしてみてください。スローされた場合は、再度検索する方法を既に知っています。合格した場合は、もう 1 つの (誤った) 保証があります。
古いときに自分自身を再発見する WebElement を使用する
WebElement
それがどのように発見されたかを記憶し、アクセスされてスローされたときに再発見するデコレータを作成します。これは明らかに、デコレーターのインスタンスを返すカスタムメソッドを使用することを強制しますfindElement()
(または、さらに良いWebDriver
ことに、通常のインスタンスfindElement()
とメソッドからインスタンスを返す装飾されたfindElemens()
メソッド)。次のようにします。
public class NeverStaleWebElement implements WebElement {
private WebElement element;
private final WebDriver driver;
private final By foundBy;
public NeverStaleWebElement(WebElement element, WebDriver driver, By foundBy) {
this.element = element;
this.driver = driver;
this.foundBy = foundBy;
}
@Override
public void click() {
try {
element.click();
} catch (StaleElementReferenceException e) {
// log exception
// assumes implicit wait, use custom findElement() methods for custom behaviour
element = driver.findElement(foundBy);
// recursion, consider a conditioned loop instead
click();
}
}
// ... similar for other methods, too
}
foundBy
これを簡単にするために、一般的な WebElements から情報にアクセスできるようにする必要があると思いますが、Selenium 開発者は、このようなことを試みるのは間違いであると考えており、この情報を公開しないことを選択したことに注意してください。古い要素を再検索することは間違いなく悪い習慣です。なぜなら、それが正当化されているかどうかをチェックするメカニズムなしで暗黙のうちに要素を再検索しているためです。再検索メカニズムは、同じ要素ではなく、完全に異なる要素を見つける可能性があります。また、見つかった要素が多数ある場合はひどく失敗しますfindElements()
( によって見つかった要素の再検索を禁止するかfindElements()
、返された から要素がいくつあったかを覚えておく必要がありますList
)。
時々役に立つと思いますが、テストの堅牢性にとって明らかにはるかに優れたソリューションであるオプション 1 と 2 を使用する人がいないことは事実です。それらを使用し、これが必要であると確信した後にのみ使用してください。
タスク キューを使用する (過去のタスクを再実行できる)
ワークフロー全体を新しい方法で実装してください!
- 実行するジョブの中央キューを作成します。このキューに過去のジョブを記憶させます。
- Command パターンの方法を使用して、必要なすべてのタスク (「要素を見つけてクリックする」、「要素を見つけてキーを送信する」など) を実装します。呼び出されたら、タスクを中央キューに追加します。中央キューは、(同期的または非同期的のいずれでも) タスクを実行します。
@LoadsNewPage
必要に応じて、すべてのタスクに@Reversible
などの注釈を付けます。
- ほとんどのタスクは例外を単独で処理するため、スタンドアロンにする必要があります。
- キューで古い要素の例外が発生すると、タスク履歴から最後のタスクが取得され、再実行されて再試行されます。
これには明らかに多大な労力が必要であり、よく考えないと、すぐに裏目に出てしまう可能性があります。テストがあったページを手動で修正した後、失敗したテストを再開するために、これの (より複雑で強力な) バリアントを使用しました。一部の条件下 (たとえば、StaleElementException
) では、失敗してもすぐにテストが終了せず、(最終的に 15 秒後にタイムアウトする前に) 待機し、情報ウィンドウがポップアップし、ユーザーに手動でテストを更新するオプションが与えられます。ページ / 右ボタンをクリック / フォームを修正 / なんでも。その後、失敗したタスクを再実行するか、履歴を数ステップさかのぼって (たとえば、最後の@LoadsNewPage
ジョブに) 戻る可能性さえあります。
最後の小ネタ
とは言っても、元のソリューションにはいくつかの研磨が必要になる可能性があります。2 つのメソッドを 1 つに結合して、より一般的なものにすることができます (または、コードの繰り返しを減らすために、少なくともこのメソッドに委譲させます)。
WebElement getStaleElem(By by, WebDriver driver) {
try {
return driver.findElement(by);
} catch (StaleElementReferenceException e) {
System.out.println("Attempting to recover from StaleElementReferenceException ...");
return getStaleElem(by, driver);
} catch (NoSuchElementException ele) {
System.out.println("Attempting to recover from NoSuchElementException ...");
return getStaleElem(by, driver);
}
}
Java 7 では、単一の multicatch ブロックでも十分です。
WebElement getStaleElem(By by, WebDriver driver) {
try {
return driver.findElement(by);
} catch (StaleElementReferenceException | NoSuchElementException e) {
System.out.println("Attempting to recover from " + e.getClass().getSimpleName() + "...");
return getStaleElem(by, driver);
}
}
このようにして、維持する必要のあるコードの量を大幅に減らすことができます。