niedziela, 12 lutego 2012

Sweet focie padającego testu selenium

Dzisiaj zamiast wywodów psychologicznych przedstawię odcinek z cyklu "poradnik młodego technika". Na tapetę pójdą testy selenium2 i generowanie "screenshotów" po błędnym teście. A że mamy już rok 2012 i JUnit wyszedł w odsłonie 4.10 to wykorzystamy sobie kilka zawartych weń bajerów.


Przykładowy test


Tutaj wielkiej filozofii nie ma. Otwieramy googla i wpisujemy nazwę jednego z ładniejszych miast w Polsce. Oczywiście test jest tak spreparowany aby nie zakończył się sukcesem (czyli zwyczajnie się wyjebie).


1:       @Test  
2:       public void shouldFindProperPageTitleAfterSearching() throws Exception {  
3:            //given  
4:            remoteDriver.get("http://www.google.com/");  
5:            String searchPhrase = "Lodz";  
6:            //when  
7:            GoogleSearchPage googleSearchPage = PageFactory.initElements(remoteDriver, GoogleSearchPage.class);  
8:            googleSearchPage.searchFor(searchPhrase);  
9:            waitForSearchResult(searchPhrase);            
10:            //then  
11:            assertThat(remoteDriver.getTitle(), containsString(searchPhrase+"aaa"));  
12:       }  
13:       private void waitForSearchResult(final String searchPhrase) {  
14:            (new WebDriverWait(remoteDriver, 2)).until(new ExpectedCondition<Boolean>() {  
15:                 public Boolean apply(WebDriver driver) {  
16:                      return driver.getTitle().toLowerCase().startsWith(searchPhrase);  
17:                 }  
18:            });  
19:       }  


I jeszcze trochę inicjalizacji.

1:     private static WebDriver remoteDriver;      
2:      @BeforeClass  
3:       public static void setUp() throws Exception {  
4:            webDriver=new FirefoxDriver();  
5:       }  
6:       @AfterClass  
7:       public static void tearDown() throws Exception {  
8:            webDriver.close();  
9:       }  

W 21 wiek


W wersji (zdaje się) 4.7 JUnita pojawił się mechanizm "Ruli" czyli takie małe AOP na potrzeby testów. W wersji 4.10 sposób użycia Ruli poprzez implementację interfejsu "MethodRule" uległ przedawnieniu a na jego miejsce pojawił się interfejs "TestRule". Jeśli ktoś nie rozumie o czym tutaj piszę to nawet i lepiej. Niechaj paczyy na kod.

 public class ScreenShotRule implements TestRule{  
      private WebDriver webDriver;  
      public ScreenShotRule(WebDriver webDriver) {  
           this.webDriver = webDriver;  
      }  
      public Statement apply(final Statement base, final Description description) {  
           return new Statement() {  
                @Override  
                public void evaluate() throws Throwable {  
                     try{  
                          base.evaluate();  
               // Tutaj można dowolnie manipulować wyjątkami dla jakich chce się robić fotki
               // Exception powinno ogarnąć zarówno problemy selenium jak i standardowe Faile testów
                     }catch (WebDriverException e) {  
                          takeScreenShot(description);  
                     }  
                }  
                private void takeScreenShot(Description description) {  
                  try {  
                     FileOutputStream out = openStreamToTargetFile(description);  
                     out.write(((TakesScreenshot)   webDriver).getScreenshotAs(OutputType.BYTES));  
                     out.close();  
                  } catch (Exception e) {  
                   throw new RuntimeException(e);  
                  }  
                }  
                private FileOutputStream openStreamToTargetFile(Description description) throws FileNotFoundException {  
                     File targetDirectory = new File("target/selenium/"+description.getTestClass().getSimpleName());  
                     targetDirectory.mkdirs();  
                     String targetFileName = description.getMethodName()+".png";   
                     return new FileOutputStream(new File(targetDirectory,targetFileName));  
                }  
           };  
      }  
 }  

Wiadomo, że w kodzie produkcyjnym trzeba by się jeszcze zająć odpowiednio zamykaniem strumieni itd. Teraz aby zastosować naszego Rula wystarczy w teście umieścić poniższe linie.


      
      @Rule  
      public static ScreenShotRule screenShotRule;  
      
      @BeforeClass  
      public static void setUp() throws Exception {  
          webDriver=new FirefoxDriver();  
          screenShotRule=new ScreenShotRule(webDriver);  
      }  

I wio. W przypadku błędnego zakończenia testu w katalogu target (jeśli tylko używamy Mavena) pojawi się katalog "selenium/nazwaTestu" z plikiem "nazwaMEtodyTestowej.png". Jeśli ktoś używa WebDrivera tylko lokalnie to to w zasadzie koniec zabawy. Jednakże czasami niektóre popularne zjebane przeglądarki nie są dostępne na np. takim ubuntu i trzeba wykorzystać zdalny WebDriver.

Tutaj zaczynają się schody...

Testowanie zdalne


Jeśli zapragniemy użyć poniższego kodu:

 
      private static WebDriver remoteDriver;  
      
      @Rule  
      public static ScreenShotRule screenShotRule;  
      
      @BeforeClass  
      public static void setUp() throws Exception {  
           remoteDriver=new RemoteWebDriver(new URL("http://localhost:3001/wd/"),firefox());  
           screenShotRule=new ScreenShotRule(remoteDriver);  
      }  

To przywita nas uroczy wyjątek:

 java.lang.RuntimeException: java.lang.ClassCastException: org.openqa.selenium.remote.RemoteWebDriver cannot be cast to org.openqa.selenium.TakesScreenshot  

Sytuacja jest o tyle dziwna, iż co jak za chwilę zobaczymy, RemoteWebDriver ma możliwość zwrócenia screenshotu jednakże z bliżej mi nieznanego powodu owa opcja nie jest udostępniona.

Czas na hak:

 private void takeScreenShot(Description description) {  
              try {  
                FileOutputStream out = openStreamToTargetFile(description);  
                WebDriver augmentedDriver = new Augmenter().augment(webDriver);  
                out.write(((TakesScreenshot) augmentedDriver).getScreenshotAs(OutputType.BYTES));  
                out.close();  
              } catch (Exception e) {  
                   throw new RuntimeException(e);  
              }  
                }  


Odpalamy i... działa!!

Ale nie do końca. Niestety jeśli teraz znowu użyjemy lokalnego WebDrivera to zauważymy, że każdy nieudany test pozostawia po sobie otwarte okno przeglądarki.

Kolejna modyfikacja.

    

FileOutputStream out = openStreamToTargetFile(description);  
                if(!(webDriver instanceof TakesScreenshot)){  
                     webDriver = new Augmenter().augment(webDriver);  
                }  
                out.write(((TakesScreenshot) webDriver).getScreenshotAs(OutputType.BYTES));  
                out.close();  

Powyższy kod spełnia swoją rolę i nie narzuca wysokiego kosztu utrzymaniowego bo pewnie nigdy tego nie będziemy modyfikować. Jednakże ów kod może powodować wrogie nastawienie fundamentalistów obiektowych.

Rozwiązanie (być może) zadowalające Policję Obiektową


 public class RemoteWebDriverWithScreenShots extends RemoteWebDriver implements TakesScreenshot{  
      public RemoteWebDriverWithScreenShots(URL url,DesiredCapabilities desiredCapabilities) {  
           super(url, desiredCapabilities);  
      }  
      public <X> X getScreenshotAs(OutputType<X> target) throws WebDriverException {  
           String base64Str = execute(DriverCommand.SCREENSHOT).getValue().toString();  
           return target.convertFromBase64Png(base64Str);  
      }  
 }  

A nasz Rul może powrócić do formy

 private void takeScreenShot(Description description) {  
             try {  
                FileOutputStream out = openStreamToTargetFile(description);  
                out.write(((TakesScreenshot) webDriver).getScreenshotAs(OutputType.BYTES));  
                out.close();  
              } catch (Exception e) {  
                   throw new RuntimeException(e);  
              }  
             }  


NOOOO

I teraz możemy powiedzieć:

Czytam Time i Epokę,
pijam tylko Ballantine'a,
palę Winstony,
programuję Obiektowo

Zdejm kapelusz