בפוסט זה אנו נבחן פונקציונליות של Selenium WebDriver הנותנת לנו את האפשרות לדווח ללוגים או לדוחות של תוצאות הריצה בצורה נקייה יותר.
נחשוב על זה ככה, אנחנו מריצים כעת כמה מקרה בדיקה עם סלניום, מקרה הבדיקה הזה מכיל בתוכו כמה שלבים (Steps), כמו למשל: ניווט לאתר מסויים, הקלקה על כפתור, הזנת טקסט לתוך שדה וכו'. אנחנו נרצה לדווח לאיזושהי מערכת לוגים \ דוחות על כל הפעולות הללו, אז כמובן אנחנו יכולים לבוא ולשלוח שורת פקודת דיווח אחרי כל שלב ושלב, אבל זה יהיה די פרימיטיבי לעשות כן, כי הקוד שיווצר לנו יהיה מנופח ובלתי קריא. לדוגמא כך (הדיווח כאן מיוצג ע"י כתיבה ל-Console):
driver.get("https://www.ebay.com/"); System.out.println("Go to Site: Ebay.com"); driver.findElement(By.id("gh-ac")).sendKeys("GoPro HERO6"); System.out.println("Insert Text into field"); driver.findElement(By.id("gh-btn")).click(); System.out.println("click on search button");
איכס, זה קוד מגעיל. מה עושים ?
ישנם כמה פתרונות אלגנטיים יותר לפתור את כל שיכפולי הקוד הללו (3 שורות קוד הפכו להיות 6 שורות קוד), אציג כאן אחד מהם. הכירו את ה-EventListeners.
* נא לא להתבלבל עם ה-TestNG Listener שגם הוא מגיע לפתור את אותה הבעיה, אך בפוסט זה אנו נתמקד דווקא בפתרון המובנה של WebDriver
ה-EventListener או בשמו המלא: WebDriverEventListener הינו ממשק (Interface) בספריות הקוד של WebDriver והוא מכיל בתוכו (נכון להיום) רשימה של 23 פעולות אשר נועדו לתפוס events (אירועים) שונים, את הרשימה הזו ניתן לראות בדוקומנטציה הרישמית של הפרוייקט.
מושג נוסף אותו אנו צריכים להכיר הינו ה-EventFiringWebDriver.
זוהי מחלקה שעוטפת את האובייקט של ה-WebDriver (איך מחלקה יכולה לעטוף אובייקט ? תראו עוד מעט בדוגמא) והיא נועדה לזרוק events שונים בזמן ריצת התוכנית, למעשה גם ה-EventFiringWebDriver מממשת את ה-WebDriver , שזה אומר שיהיו לה את כל הפונקציונליות של, נגיד ה-ChromeDriver (כמו findElement , getTitle , switchTo וכו') ובנוסף עוד 2 פעולות: ה-register וה-unregister
פעולת ה-register תאפשר לנו להירשם להאזנה של events ב-WebDriverEventListener ופעולת ה-unregister תספיק את ההאזנה
לסיכום: ה-EventFiringWebDriver זורק events וה-WebDriverEventListener תופס אותם.
את ההירארכיה של הממשקים\מחלקות הללו ניתן לראות כאן:
את המימוש לכך נבצע בכמה שלבים:
1. איתחול אובייקט ה-driver שלנו מסוג WebDriver באופן הרגיל, למשל נאתחל אותו ל-ChromeDriver לעבודה מול דפדפן הגוגל כרום:
WebDriver chDriver = new ChromeDriver();
2. ניצור אובייקט חדש מסוג EventFiringWebDriver – זוהי המחלקה המממשת את ה-WebDriver, נקרא לו driver ונשלח לבנאי שלו את אובייקט ה-chDriver שיצרנו מקודם (או במילים אחרות אנחנו עוטפים את אובייקט ה-driver במחלקה חדשה -EventFiringWebDriver):
EventFiringWebDriver driver = new EventFiringWebDriver(chDriver);
3. ניצור אובייקט ממחלקה (נקרא לה eventImplementations) בה אנו נממש את כל המתודות של WebDriverEventListener (את המחלקה והמימושים נראה עוד מעט, כעת אנו רק ניצור את האובייקט שלה):
eventImplementations ei = new eventImplementations();
4. נבצע כעת רישום של האובייקט מסוג WebDriverEventListener (אותו יצרנו בסעיף הקודם) ע"י קריאה למתודת ה-register (ששייכת כאמור למחלקת EventFiringWebDriver):
driver.register(ei);
בואו נראה כעת את המחלקה המממשת את מתודות ה-WebDriverEventListener , המימוש בדוגמא הזו מתייחס כעת לפעולות: beforeClickOn , afterClickOn , beforeNavigateTo , afterNavigateTo , onException afterChangeValueOf (זאת אומרת שהחלטתי לממש רק 6 פעולות מתוך ה-23)
import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.events.WebDriverEventListener; public class eventImplementations implements WebDriverEventListener { public void beforeClickOn(WebElement arg0, WebDriver arg1) { System.out.println("Before Clicking On " + arg0.getAttribute("value")); } public void afterClickOn(WebElement arg0, WebDriver arg1) { System.out.println("After Clicking On Element"); } public void beforeNavigateTo(String arg0, WebDriver arg1) { System.out.println("Before Navigate To " + arg0); } public void afterNavigateTo(String arg0, WebDriver arg1) { System.out.println("After Navigate To " + arg0); } public void afterChangeValueOf(WebElement arg0, WebDriver arg1, CharSequence[] ch) { System.out.println("Element: \"" + arg0.getAttribute("placeholder") + "\" was updated to: " + arg0.getAttribute("value")); } public void afterAlertDismiss(WebDriver arg1) { // TODO Implement this Method } public void beforeAlertDismiss(WebDriver arg1) { // TODO Implement this Method } public void afterAlertAccept(WebDriver arg1) { // TODO Implement this Method } public void beforeAlertAccept(WebDriver arg1) { // TODO Implement this Method } public void afterFindBy(By arg0, WebElement arg1, WebDriver arg2) { // TODO Implement this Method } public void afterNavigateBack(WebDriver arg0) { // TODO Implement this Method } public void afterNavigateForward(WebDriver arg0) { // TODO Implement this Method } public void afterNavigateRefresh(WebDriver arg0) { // TODO Implement this Method } public void afterScript(String arg0, WebDriver arg1) { // TODO Auto-generated method stub } public void beforeChangeValueOf(WebElement arg0, WebDriver arg1, CharSequence[] ch) { // TODO Implement this Method } public void beforeFindBy(By arg0, WebElement arg1, WebDriver arg2) { // TODO Implement this Method } public void beforeNavigateBack(WebDriver arg0) { // TODO Implement this Method } public void beforeNavigateForward(WebDriver arg0) { // TODO Implement this Method } public void beforeNavigateRefresh(WebDriver arg0) { // TODO Implement this Method } public void beforeScript(String arg0, WebDriver arg1) { // TODO Implement this Method } public void onException(Throwable arg0, WebDriver arg1) { System.out.println("See Error: " + arg0.getMessage()); } }
טוב, מה זה אומר ? זה אומר שכעת כשאנו נפעיל את ה-driver שלנו, יתבצעו הפעולות הבאות:
לפני כל הקלקה של העכבר יודפס ל-Console הטקסט: Before Clicking On עם שירשור של שם הכפתור
אחרי כל הקלקה של העכבר יודפס ל-Console הטקסט: After Clicking On Element
לפני ניווט לאתר יודפס ל-Console הטקסט: Before Navigate To עם שירשור כתובת האתר
אחרי ניווט לאתר יודפס ל-Console הטקסט: After Navigate To עם שירשור כתובת האתר
אחרי שליחת טקסט לאלמנט (פעולת ה-sendKeys שאנו מכירים) יודפס ל-Console טקסט בהתאם
וברגע שהטסט יכשל ויזרק Exception, תודפס ההודעה של ה-Exception ל-Console
עכשיו נותר לנו רק לממש את הכל ביחד תחת מחלקת ה-Tests שלנו, מקרה הבדיקה שלנו הינו: כניסה לאתר ebay , הזנת הערך: GoPro HERO6 בשדה החיפוש ולחיצה על כפתור ה-Search:
import java.util.concurrent.TimeUnit; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.support.events.EventFiringWebDriver; public class testCases extends init { private static WebDriver chDriver; private static EventFiringWebDriver driver; private static eventImplementations ei; @BeforeClass public static void openBrowser() { ei = new eventImplementations(); System.setProperty("webdriver.chrome.driver", "C:/chromedriver.exe"); chDriver = new ChromeDriver(); driver = new EventFiringWebDriver(chDriver); driver.register(ei); driver.manage().window().maximize(); driver.manage().timeouts().implicitlyWait(60, TimeUnit.SECONDS); driver.get("https://www.ebay.com/"); } @Test public void myScenario1() throws InterruptedException { driver.findElement(By.id("gh-ac")).sendKeys("GoPro HERO6"); driver.findElement(By.id("gh-btn")).click(); } @AfterClass public static void closeBrowser() { driver.quit(); } }
מאחורי הקלעים התבצעו הפעולות הבאות.
פקודת ה-driver.get זרקה event שנתפס ב-beforeNavigateTo וב-afterNavigateTo (והכתיבה ל-Console הייתה בהתאם)
פקודת ה-sendKeys זרקה event שנתפס ב-afterChangeValueOf (והכתיבה ל-Console הייתה בהתאם)
פקודת ה-click זרקה event שנתפס ב-beforeClickOn וב-afterClickOn (והכתיבה ל-Console הייתה בהתאם)
ניתן כמובן גם להרחיב את היכולות של הדיווח ובמקום לרשום ל-Console (כי מי באמת רושם לConsole בפרוייקט אוטומציה אמיתי ?) ניתן לכתוב ל-Reporting System ע"י שימוש ב-API חיצוני כמו ה-Allure או ה-ExtentReports, זה אומר שאנו נהיה חייבים לאתחל את האובייקטים של ה-reports בצורה חכמה יותר ממחלקת ה-eventImplementations שלנו.
למשל נוכל לרשת ממחלקה אחרת ה-base או ה-init שם אנו נדאג לאיתחולים של האובייקטים, כך למשל:
public class eventImplementations extends init implements WebDriverEventListener
ומחלקת ה-init שלנו למשל תוכל להיראות כך (במקרה הזה אני מציג דוגמא לכתיבה לתוך קובץ חיצוני):
import java.io.FileNotFoundException; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; public class init { private static String logFile = "D:/test/MyLogFile.log"; protected static PrintWriter writer; protected static void CreateLog() throws FileNotFoundException, UnsupportedEncodingException { writer = new PrintWriter(logFile, "UTF-8"); } protected static void CloseLog() throws FileNotFoundException, UnsupportedEncodingException { writer.close(); } protected void WriteToLog(String value) throws FileNotFoundException, UnsupportedEncodingException { writer.println(value); } }
עכשיו מה לגביי מחלקת ה-Tests שלנו ? האם ככה באמת נכתוב אותה בפרוייקט שלנו ? איפה ה-Page Objects למשל, איפה האבסטרקציה ? או במילים אחרות – האם ה-Event Listeners תומכים בכתיבה נכונה של תשתיות ? התשובה היא ברור שכן. בסופו של דבר נורה איזשהו event , לא משנה מהיכן בקוד או מאיזו מחלקה (של תשתית או של ביזנס).
לדוגמא, אותו מקרה בדיקה שכתבנו מקודם, נוכל לכתוב כך (עם Page Objects) והכתיבות ל-Console עדיין יעבדו כמו קסם:
מחלקת ה-Pages:
import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.How; public class mainPage { @FindBy(how = How.ID, using = "gh-ac") public WebElement searchField; @FindBy(how = How.ID, using = "gh-btn") public WebElement searchButton; public void search(String item) { searchField.sendKeys(item); searchButton.click(); } }
מחלקת הטסטים:
import java.util.concurrent.TimeUnit; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.support.PageFactory; import org.openqa.selenium.support.events.EventFiringWebDriver; public class testCases extends init { private static WebDriver chDriver; public static EventFiringWebDriver driver; public static eventImplementations ei; public static mainPage mp; @BeforeClass public static void openBrowser() { ei = new eventImplementations(); System.setProperty("webdriver.chrome.driver", "C:/chromedriver.exe"); chDriver = new ChromeDriver(); driver = new EventFiringWebDriver(chDriver); driver.register(ei); driver.manage().window().maximize(); driver.manage().timeouts().implicitlyWait(60, TimeUnit.SECONDS); driver.get("https://www.ebay.com/"); mp = PageFactory.initElements(driver, mainPage.class); } @Test public void myScenario1() throws InterruptedException { mp.search("GoPro HERO6"); } @AfterClass public static void closeBrowser() { driver.quit(); } }
בשורה התחתונה , עבודה עם Listeners היא אינטליגנטית, נקייה, ופותרת לנו לא מעט כאבי ראש במימושים שונים לדיווחים. מצד שני, כמו שנכתב כבר ישנם כיום מימושים ל-23 פעולות ב-WebDriverEventListener (החל מגרסת 3.10 של סלניום התווספו עוד שניים: beforeSwitchToWindow ו-afterSwitchToWindow) , זה יפה אבל לא מספיק, ישנן עוד פעולות רבות שאין להם מימושים כמו בחירת ערך מתוך Drop Down או גרירת אובייקטים עם Drag N Drop וכו', זה אומר שלפעולות אלו נצטרך לבצע מימושים משל עצמינו וזה קצת מבאס…