רגע, מה ? כן, לא התבלבלנו, מה שקראתם, בפוסט הזה אנחנו הולכים לדבר על תבניות קשטן (מלשון "קישוט"). וכאן הזמן להגיד כי זהו אחד מהמקרים בהם עדיף לנו לא לתרגם כל דבר לעברית 🙂

ה-Decorator Design Pattern הינה תבנית עיצוב שמאוד כדאי לנו להשתמש בה בתשתית אוטומציה (וישנם אפילו כאלו שכותבים על פיה ואינם מודעים לכך), בגדול היא אומרת דבר כזה: אנחנו יכולים להוסיף התנהגויות נוספות לאובייקטים קיימים שיוני הסביר בסרטון ה-Design Patterns בעולם האוטומציה ע"י הדוגמא של הגלידה (דקה: 22:24 בסרטון)

 

למעשה תבנית הקשטן (Decorator) תמיד מזכירה לי את סט בובות המטריושקה המוכר לנו שם ישנה בובה של סבתא בתוך סבתא בתוך סבתא, איך בדיוק ? במקרה שלנו אנו נרצה לעטוף אובייקטים בתבניות עוטפות (wrappers) ואותם נוכל לעטוף בעוד שכבה וכו'.

 

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

תתארו לכם כי אנו מעוניינים לעבוד עם מחלקה שנקראת SetEnvironment , מחלקה זו מאתחלת לי את הסביבה לקראת ההרצה של הבדיקות, אחת מהמתודות שם תיקרא: openClient שתפקידה הוא לפתוח את הקליינט עליו נרצה להריץ את הבדיקות שלנו, הקליינט יכול להיות למשל דפדפן הכרום, דפדפן אחר, אפליקציית אלקטרון, אפליקציית נייטיב באנדרואיד וכו'.

עד פה נשמע סביר לחלוטין וגם לא קשה למימוש. סביר להניח כי הדבר הראשון שיקפוץ לנו לראש יהיה להשתמש בהורשה כך שמחלקת ה-SetChrome למשל תדאג להפעיל את דפדפן הכרום, מחלקת ה-SetElectron תדאג להפעיל את אפליקציית אלקטרון וכן הלאה… וכל אלו יירשו מאותה מחלקת בסיס SetEnvironment

 

מגניב, אפשר עכשיו לסגור את הפוסט ? לא בדיוק.

מה יקרה למשל אם בעוד שבועיים מנהל ה-QA יבוא אלינו וירצה להריץ במקביל את סט בדיקות הריגרסיה שלנו גם על דפדפן האדג' וגם על אפליקציית נייטיב על אנדרואיד ? או על שני דפדפנים – בכרום ואדג' ?

מה הבעיה ? ניצור מחלקות נוספות של שילוב בין השניים או אפילו שלושה או ארבעה… אוקיי, זהו בהחלט פתרון, אבל למעשה הוא די עקום כי ע"י שימוש בהורשה כפי שאנחנו השתמשנו בו, נאלץ להרחיב עוד ועוד את הפרוייקט שלנו, לסרבל אותו ולהפוך אותו לזוועת עולם בעתיד הלא רחוק, במילים אחרות: לא ה-Best Practice לבניית תשתית אוטומציה.

אנחנו צריכים לחשוב על דרך לשנות את התנהגות האובייקט (של מחלקת SetEnvironment) שלנו. פתרון ההורשה פחות מתאים לנו כאן, למה ?

כי הורשה היא סטאטית. אנחנו לא יכולים לשנות את התנהגות האובייקט בזמן ריצה, אנחנו רק יכולים להחליף את האובייקט כולו באובייקט אחר עם התנהגות שונה אשר נוצרה ממחלקה שונה וכי מחלקות הבנים של ההורשה אינם יכולים לרשת מכמה מחלקות אב (בהרבה מהשפות), לכל מחלקה נוכל להגדיר רק SuperClass אחד שזה אומר מקור אחד של התנהגויות.

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

ניתן לקרוא למחלקה שעליה נצביע – מחלקה עוטפת או Wrapper Class (יש כאלו שקוראים לה גם Helper Class), כך שאובייקט יכול להשתמש בהתנהגויות שונות מכמה מחלקות (זיכרו – בהורשה לא ניתן לרשת מכמה מחלקות בהרבה שפות) בעזרת ההצבעה והיכולת כעת להפעיל פעולות שונות. הקומפוזיצה מאפשרת לנו להשתמש ב-Decorator Pattern בצורה מאוד הגיונית.

 

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

ישנם כאן דברים הדורשים עוד שיפור ותוספות (כמו שימוש ב-Report , שימוש ב-Event Listeners ועוד…), אבל זוהי נקודת התחלה טובה לנסות להעביר את המסר. אמנם זוהי אינה דוגמא של Propper תבנית עיצוב של Decorator ובעולם האמיתי אנחנו גם משלבים כמה patterns ביחד עד כמה שניתן כמובן, מבצעים עידכונים לצרכים שלנו תוך כדי שמנסים לא לשבור את כללי האצבע של תכנות נכון כמובן. (אגב, בקורס ה-Automation Plus Plus שלנו אנו מסבירים שלא ניתן להחיל את כל ה-Design Patterns ביחד, וישנן התנגשויות בין כמה)

תחילה נראה את מחלקת ה-Wrapper:

public class DriverWrapper {
    private final WebDriver webDriver;
    private final Browsers browser;
    private final WebDriverWait wait;

    public DriverWrapper(Browsers browser, String url) {
        this.browser = browser;
        switch (this.browser) {
            case CHROME:
                WebDriverManager.chromedriver().setup();
                webDriver = new ChromeDriver();
                break;
            case FIREFOX:
                WebDriverManager.firefoxdriver().setup();
                webDriver = new FirefoxDriver();
                break;
            case EDGE:
                WebDriverManager.edgedriver().setup();
                webDriver = new EdgeDriver();
                break;
            default:
                throw new RuntimeException("Illegal Argument");
        }
        webDriver.manage().window().maximize();
        webDriver.get(url);
        webDriver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5));
        wait = new WebDriverWait(webDriver, Duration.ofSeconds(5));
    }

    public void goTo(String url) {
        webDriver.get(url);
    }

    public WebElement getElement(By elem) {
        wait.until(ExpectedConditions.visibilityOfElementLocated(elem));
        return webDriver.findElement(elem);
    }

    public List getElements(By elems) {
        wait.until((d) -> this.webDriver.findElements(elems).size() > 1);
        return webDriver.findElements(elems);
    }

    public boolean click(By elem) {
        String text = "";
        boolean succeed = false;
        try {
            wait.until(ExpectedConditions.elementToBeClickable(elem));
            text = getElement(elem).getText();
            getElement(elem).click();
            System.out.println("Call Report: Element " + text + " was clicked successfully");
            succeed = true;
        }
        catch (Exception e) {
            System.out.println("Call Report: Failed to click on Element: " + text + " , see details: " + e);
            fail("Failed to click on Element: " + text);
        }
        return succeed;
    }

    public boolean updateText(By elem, String value) {
        boolean succeed = false;
        try {
            getElement(elem).clear();
            getElement(elem).sendKeys(value);
            System.out.println("Call Report: Value " + value + " was inserted successfully");
            succeed = true;
        }
        catch (Exception e) {
            System.out.println("Call Report: Failed to insert to text field the value " + value + " , see details: " + e);
            fail("Failed to insert to text field the value " + value + " , see details: " + e);
        }
        return succeed;
    }

    public boolean quit() {
        boolean succeed = false;
        try {
            webDriver.quit();
            System.out.println("browser quit successfully");
            succeed = true;
        }
        catch (Exception e) {
            System.out.println("Failed to quit the browser, see details " + e.getMessage());
            fail("Failed to quit the browser, see details " + e.getMessage());
        }
        return succeed;
    }

    public void verifyEquals(int actual, int expected) {
        try {
            assertEquals(actual, expected);
            System.out.println("Call Report: verify passed, actual (" + actual + ") matches expected (" + expected + ")");
        }
        catch (AssertionError e) {
            System.out.println("Call Report: verify failed, actual (" + actual + ") does not matches expected (" + expected + ")");
            fail("Call Report: verify failed, actual (" + actual + ") does not matches expected (" + expected + ")");
        }
    }

    public void verifyNumberOfSearchProducts(By elems, int expected) {
        int actual = getElements(elems).size();
        verifyEquals(actual, expected);
    }
}

את המחלקת הטסטים שלנו נוכל לממש כך:

public class TestCase {
    private DriverWrapper browser;

    @BeforeClass
    public void startSession() {
        browser = new DriverWrapper(Browsers.FIREFOX, "https://atid.store/");
    }

    @Test
    public void test01() {
        // another navigation option:
        browser.goTo("https://atid.store/");
        browser.click(MainPage.menu_store);
        browser.updateText(StorePage.search_field, "Jeans");
        browser.click(StorePage.search_button);
        browser.verifyNumberOfSearchProducts(StorePage.products, 7);
    }

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

* העבודה עם Page Objects פחות מעניינת אותנו בפוסט הזה וכמו כן עבודה עם Enum שמייצג את הדפדפנים (גם זה פחות חשוב כעת)

* המתודות של ה-Wrapper מחזירות ערך בוליאני שמא אולי נרצה לדווח עליהם אח"כ ללוג כזה או אחר, למשל:

if (browser.click(MainPage.menu_store)) {
    System.out.println("clicked ok");
}
else {
    System.out.println("failed to click");
}

או אולי אפילו כך:

System.out.println(browser.quit());

בדוגמא הזו אני משתמש ב-Wrapper אחד, אבל בעולם האמיתי יכול מאוד שנרצה לעבוד עם כמה מחלקות \ אובייקטים שאחראים על דברים שונים, הטסט שלנו יכול לקרוא לאובייקטים שונים, ואם במידה ונרצה להוסיף התנהגות לאחת המקומפוננטות ניתן לעשות זאת כך שתיראה באופן שקוף מהצד של מקרה הבדיקה.

אגב, את מקרה המטריושקה שנתתי מקודם, ניתן להמשיל כאן כך ש-wrapper של כרום יוכל בתוך wrapper של browser וזה יוכל בתוך warpper של client וכן הלאה…

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