בפוסט הזה אני מעוניין לדבר אתכם על בעיה נפוצה באוטומציה על אפליקציות Ui , כאשר אנו עובדים עם אלמנטים (נגיד עם Selenium על ה-Web) יכול לקרות מצב שבו האלמנטים הפכו להיות Stale (שזה אומר שתוקפם פג. הם היו זמינים אך כעת כבר לא).

לקריאה נוספת אודות Stale Element Reference Exception אתם מוזמנים לקרוא את הפוסט של יוני פלנר שהוציא לפני כמה שנים כאן בבלוג.

 

בואו נראה כעת דוגמא מהשטח להופעת האלמנטים שתוקפם פג בדף ה-HTML. נגיד ונרצה להיכנס לאתר התירגולים שלנו: ATID Store , ולחפש שם 2 מוצרים: Jeans ו-T-Shirts.

כך יראו האלמנטים שלנו של שדה החיפוש + כפתור החיפוש:

המלבן האדום מייצג לנו את האלמנט של שדה החיפוש ואילו המלבן הירוק מייצג לנו את כפתור החיפוש. זאת אומרת שאנו יכולים לזהות אותם עם Selenium לפי הדרך הבאה:

WebElement searchField = driver.findElement(By.id("wc-block-search__input-1"));
WebElement searchButton = driver.findElement(By.cssSelector("button[aria-label='Search']"));

כעת אם נפעיל את מתודת ה-sendKeys על אלמנט ה-search field ונכניס לו את הערך Jeans ואח"כ נלחץ על אלמנט ה-search button , סלניום אמור לבצע את החיפוש ואכן, אנו נראה את תוצאות החיפוש.

בחיפוש השני, שוב פעם נקרא לאלמנט ה-search field ונכניס ל-sendKeys שלו את הפעם הערך tshirt , מה יקרה במצב כזה ? האם נצליח להזין את הערך ?

אז התשובה היא – לא ! במצב כזה אנו נקבל Stale Element Reference Exception כשננסה להריץ את התוכנית. לפני שנדבר על למה זה קורה, בוא נראה את התוכנית המלאה (זיכרו, אנו מעוניינים לחפש במקרה הבדיקה שני מוצרים, האחד – Jeans והשני – T-Shirt):

public class AtidStore {

    private WebDriver driver;

    @BeforeClass
    public void openBrowser() {
        driver = new ChromeDriver();
        driver.manage().window().maximize();
        driver.get("https://atid.store/store/");
        driver.manage().timeouts().implicitlyWait(3, TimeUnit.SECONDS);
    }

    @AfterClass
    public void closeBrowser() {
        driver.quit();
    }

    @Test
    public void test01_Search() {
        WebElement searchField = driver.findElement(By.id("wc-block-search__input-1"));
        WebElement searchButton = driver.findElement(By.cssSelector("button[aria-label='Search']"));

        searchField.sendKeys("Jeans");
        searchButton.click();

        searchField.sendKeys("tshirt");
        searchButton.click();
    }
}

שימו לב כי במקרה הבדיקה יש לנו 2 קריאות לאלמנט ה-searchField ו-2 קריאות לאלמנט ה-searchButton. כאשר ההבדל ביניהם הוא עידכון דף ה-HTML. מה הכוונה ?

אחרי שאנו לוחצים (click) על כפתור החיפוש בפעם הראשונה, דף ה-HTML מתרענן וכעת הוא מציג לנו את תוצאות החיפוש המסוננות (לפי Jeans), זהו אינו אותו DOM (Document Object Model) כפי שהוצג לנו בהתחלה ועליו זיהינו את האלמנטים, אלא DOM חדש. על כן אותם אלמנטים שהיו מזוהים בעבר, כבר לא בתוקף החל מעידכון העמוד. זה אומר שכשנריץ את התוכנית, בשורת הפקודה: searchField.sendKeys("tshirt"); אנחנו נקבל את ה-Exception שהאלמנט כבר לא בתוקף.

 

מה הפתרון ?

פתרון אחד שניתן לחשוב עליו הוא פשוט לא לשמור את הזיהויים של האלמנטים לתוך משתנים אלא לזהות אותם בכל קריאה מחדש ולהפעיל עליהם את הפעולות, כך לדוגמא:

@Test
public void test01_Search() {
    driver.findElement(By.id("wc-block-search__input-1")).sendKeys("Jeans");
    driver.findElement(By.cssSelector("button[aria-label='Search']")).click();

    driver.findElement(By.id("wc-block-search__input-1")).sendKeys("tshirt");
    driver.findElement(By.cssSelector("button[aria-label='Search']")).click();
}

החדשות הטובות הן שאופציה זו תעבוד לנו ולא נקבל כך אלמנטים שהם Stale , החדשות הרעות – זוהי כתיבה גרועה מאוד לכתוב את הבדיקות שלנו, למה ? בעיקר בגלל העניין התחזוקתי של הפרוייקט, בדוגמא שלנו, אנחנו רואים שאנו קוראים פעמיים לאלמנט אחד, נגיד שדה החיפוש עם ה-id שלו ששווה ל-wc-block-search__input-1 , יפה, אך מה יקרה בעוד כשבוע כאשר המפתחים ישנו את המאפיין הזה ? בכמה מקומות מפתחי האוטומציה יצטרכו לעדכן את הקוד שלהם בהתאם ? בדוגמא הזו פעמיים, אבל בפרוייקט אוטומציה אמיתי זה יכול להגיע לעשרות ואפילו מאות מקומות שונים… מממ… בעיה.

 

פתרון נוסף (הרבה יותר הגיוני ואינטיליגנטי) הוא להשתמש ב-Page Factory, הרי אנחנו מכירים את ה-Page Factory עוד מהשיעורים הראשונים שלמדנו… מממ… בואו נגיד מ-YouTube… לא ?

יפה, אז לא הרבה יודעים זאת, אבל ה Page Objects Model עובד בצורה כזו שהוא מונע את השגיאות הללו של Stale כשאנו מגדירים את האלמנטים שלנו ב-repository שלו עם אנוטציית ה-FindBy, ה-Page Factory הוא המימוש של ה-Page Objects Model שמוכר לחלקינו.

זיהוי אלמנט יתבצע כאמור עם אנוטציית ה-FindBy, בדוגמא שלנו עם שדה החיפוש, זה יראה כך:

@FindBy(how = How.ID, using = "wc-block-search__input-1")
protected WebElement searchField;

ה-PageFactory הינה מחלקה אשר ניתן ממנה ליצור אובייקטים ומאפשרת את גישת ה-Lazy Proxy (שהאלמנטים ייטענו בצורה עצלה רק ברגע שנקרא להם ע"י מתודות) – גישה זו היא חלק מה-Proxy Pattern אשר מפתחי סלניום עושים בה שימוש תוך מימוש ה-Page Factory.

את ה-Lazy Proxy אנו נוכל להחיל על כל אחד מהאלמנטים עימם אנו נרצה לעבוד, במקרה שלנו הם ה-searchField וה-searchButton

עבודה בצורה כזו תחייב אותנו כמובן לאתחל את כל האלמנטים שאנו מגדירים, ע"י קריאה לפעולת ה-initElements (ממחלקת ה-PageFactory) תוך שליחה של שם המחלקה שתהווה את ה-scope מולה נרצה לעבוד ולאתחל.

הרעיון כאן הוא שהאלמנטים יזוהו כל פעם מחדש בכל קריאה למתודה על האלמנט. זאת אומרת – במידה ולחצנו על כפתור 10 פעמים, אז האלמנט יזוהה 10 פעמים רגע לפני כל לחיצה.

להלן הקוד של מחלקת ה-Page Objects וזיהויי שני האלמנטים:

class PageObjects {
    private WebDriver driver;

    PageObjects(WebDriver driver) {
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }

    @FindBy(how = How.ID, using = "wc-block-search__input-1")
    protected WebElement searchField;

    @FindBy(how = How.CSS, using = "button[aria-label='Search']")
    protected WebElement searchButton;
}

ואילו כאן נוכל לראות את מחלקת הבדיקות שלנו מעודכנת לעבודה מול Page Objects:

public class AtidStore {

    private WebDriver driver;
    private PageObjects pageObjects;

    @BeforeClass
    public void openBrowser() {
        driver = new ChromeDriver();
        driver.manage().window().maximize();
        driver.get("https://atid.store/store/");
        driver.manage().timeouts().implicitlyWait(3, TimeUnit.SECONDS);
        pageObjects = new PageObjects(driver);
    }

    @AfterClass
    public void closeBrowser() {
        driver.quit();
    }

    @Test
    public void test01_Search() {
        pageObjects.searchField.sendKeys("Jeans");
        pageObjects.searchButton.click();

        pageObjects.searchField.sendKeys("tshirt");
        pageObjects.searchButton.click();
    }
}

מה אנחנו רואים כאן ?

בסופו של דבר אנו מחפשים את 2 הפריטים ע"י השמה ושמירה של אלמנטים למקום מרכזי אחד (דרך ה-Page Objects) ובכך שומרים על יכולות תחזוקה גבוהות של הפרוייקט ומצד שני, איננו חוטפים שגיאות של Stale על אותם אלמנטים מכיוון שהם מזוהים בכל פעם החדש.

 

גם את הפרק של Page Objects למתקדמים עם Stale , אנוטציות נוספות ועוד, אתם מוזמנים לראות בקורס הדיגיטלי שלנו: Automation Plus Plus

 

בהצלחה,

סאיד

השאר הערה\הודעה