Tirando screenshots de testes que falharam no JUnit

Obter screenshot de um teste que falhou é um recurso muito útil quando trabalhamos com testes pela UI. Neste post, vamos ver como implementar esse recurso com o Selenium WebDriver no JUnit. Mesmo que seja algo fácil de ser implementado, temos que prestar atenção em alguns pontos.

Motivação: por que seria útil tirar screenshot de um teste que falhou?

 

1. Logs de erro, muitas vezes, não são tão claros assim

Um exemplo bem explicativo é quando um teste falha devido a uma NoSuchElementException. Se o teste lança essa exceção, é porque um determinado elemento não está aparecendo na tela, certo? Não necessariamente.

Causas possíveis:

  • O elemento realmente não existe na tela, mas deveria.

  • O locator do elemento está errado. É comum acabarmos cometendo um erro de digitação (principalmente quando trabalhamos com locators mais extensos) ou vermos errado o identificador de algum elemento.

  • O teste procurou o elemento na tela errada. Imagine um teste em uma tela de login (com campos "Usuário" e "Senha"). Agora imagine que o teste anterior não deu um logout na aplicação, e quando seu teste abriu a URL da mesma, foi redirecionado pra uma HomePage (ou uma outra página que não seja a de login). O teste tentaria buscar os campos para fazer login, e não os encontraria.

2. Redução do tempo de investigação de falhas
  • Com o log e o screenshot da tela no momento em que o teste falhou, fica MUITO mais fácil descobrir o que realmente aconteceu de errado.

  • Esse recurso é essencial quando não estamos "vendo" a execução dos testes, seja por estarmos executando os testes em outra máquina (com o Selenium Grid e um servidor de Integração Contínua, por exemplo), ou executando em um Headless Browser.


Como tirar um screenshot com o Selenium WebDriver

A API do Selenium WebDriver provê a interface TakesScreenshot, que é implementada pelos drivers de cada browser (e.g. FirefoxDriver, ChromeDriver, InternetExplorerDriver...), exceto pelo HtmlUnitDriver, que não suporta screenshots.

Para obter um screenshot, basta fazer:

WebDriver driver = new FirefoxDriver();

//As duas formas mais usadas para obter um screenshot são:
//obter um objeto File OU um array de bytes
File screenshotFile = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);  
byte[] screenshotByteArray = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);

//Salva um screenshot do tipo File, usando a classe
//FileUtils (org.apache.commons.io.FileUtils)
FileUtils.copyFile(screenshotFile, new File("screenshot_com_file.png"));

//Salva um screenshot de byte[], usando a classe
//FileOutputStream (java.io.FileOutputStream)
FileOutputStream outputStream = new FileOutputStream("screenshot_com_byte_array.png");  
outputStream.write(screenshotByteArray);  
outputStream.close();  

OBS: Até a versão 2.40.0 do Selenium WebDriver, o RemoteWebDriver (usado com o Selenium Grid) não implementava a interface TakesScreenshot. Com isso, era preciso utilizar a classe Augmenter, que adiciona os métodos da TakesScreenshot à instância do driver. O código ficaria então da seguinte forma:

//Cria uma instância do RemoteWebDriver
DesiredCapabilities dcaps = DesiredCapabilities.chrome();  
URL url = new URL("http://localhost:4444/wd/hub");  
WebDriver driver = new RemoteWebDriver(url, dcaps);

//Faz um "augment" no driver
WebDriver augmentedDriver = new Augmenter().augment(driver);

//Tira e salva o screenshot
File screenshot = ((TakesScreenshot)augmentedDriver).getScreenshotAs(OutputType.FILE);  
FileUtils.copyFile(screenshotFile, new File("screenshot.png"));  

Solução no JUnit

Agora que sabemos tirar screenshots com o Selenium WebDriver, veremos como aplicar isso ao ciclo de vida do JUnit, de forma que possamos obter um screenshot da tela no momento em que um teste falha.

JUnit Rules

Nas versões mais recentes do JUnit, existe o conceito de rules. Rules podem ser vistas como interceptors do JUnit, e seu uso traz mais flexibilidade na escrita dos seus testes. Para o nosso objetivo, vamos criar uma rule que estende a classe TestWatcher. Com isso, vamos poder ter controle sobre a execução dos testes, podendo alterar o comportamento de um teste quando o mesmo passa ou falha, dentre outros. Pode parecer complicado, mas com o código dá para entender melhor:

public class ScreenshotRule extends TestWatcher  
{
   @Override
   protected void failed(Throwable e, Description description)
   {
      //O que colocarmos aqui será executado sempre que um teste falhar...
      //É aqui que vamos colocar o método de tirar screenshot!
   }

   @Override
   protected void succeeded(Description description)
   {
      //O que colocarmos aqui será executado sempre que um teste passar...
      //Se colocarmos o método de tirar screenshot aqui, teremos um
      //screenshot de cada teste que passou!
   }
}

Vamos agora criar um método encapsulando a funcionalidade de screenshots do Selenium WebDriver. Uma coisa que acho interessante fazer é colocar, no nome do arquivo do screenshot, o nome da classe e do método de teste. Isso facilita a organização dos screenshots e garante que você não vai se perder para saber qual imagem pertence a cada teste que falhou. Os nomes da classe e do método de teste podem ser obtidos através do objeto do tipo Description, que é argumento dos métodos failed e succeeded.

O método abaixo cria um diretório chamado "screenshots" na raiz do projeto, obtém um screenshot, e cria um arquivo .png com o nome da classe/método de teste.

public void tiraScreenshot(String nomeClasse, String nomeTeste)  
{
   try {
      //Cria um diretório "screenshots" na raiz do projeto
      new File("screenshots/").mkdirs();

      //Obtém um screenshot
      File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);

      //Cria um arquivo dentro do diretório "screenshots", contendo
      //o nome da classe/método de teste
      //Exemplo: "IncluirUsuarioTest-incluirComSucesso-screenshot.png"
      FileUtils.copyFile(screenshot, new File("screenshots/" + nomeClasse + "-" + nomeTeste + "-screenshot.png"));

   } catch(Exception e) {
   }
}

A rule, com o método de screenshot, ficaria então da seguinte forma:

public class ScreenshotRule extends TestWatcher  
{
   @Override
   protected void failed(Throwable e, Description description)
   {
      String nomeClasse = description.getTestClass().getSimpleName();
      String nomeTeste = description.getMethodName();

      tiraScreenshot(nomeClasse, nomeTeste);
   }

   public void tiraScreenshot(String nomeClasse, String nomeTeste)
   {
      //Criar diretório "screenshots", obter screenshot e criar arquivo .png
   }
}

OBS: Uma outra sugestão interessante é colocar, no nome do arquivo, a data/hora atual. Basta usar uma API de datas do Java, como o Calendar. :)

Como associar a Rule aos seus testes?

Para associar a rule recém-criada com seus testes, basta criar um atributo público anotado com @Rule. O ideal é colocar esse atributo na sua classe base de teste, como podemos ver abaixo:

public class BaseTest  
{
   protected static WebDriver driver;

   @Rule
   public ScreenshotRule screenshotRule = new ScreenshotRule();

   @BeforeClass
   public static void setUp()
   {
      //Criar instância do driver desejado
   }

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

Pronto! A rule funcionará para toda classe de teste que estender essa classe base.


Problema: métodos com @After

Caso seus testes possuam algum método anotado com @After, ou seja, um método executado após a execução de cada método de teste (um método que faça logout após cada teste, por exemplo), a rule não terá muita utilidade. Isso se deve ao fato de que a rule, no ciclo de vida do JUnit, é executada sempre antes de um método com @Before e depois de um método com @After. Dessa forma, a rule tiraria um screenshot do estado da tela após o método @After ter sido executado. Essa resposta do StackOverflow explica bem como isso acontece: http://stackoverflow.com/a/12102173

Como solução para esse problema, podemos seguir a mesma ideia que usamos na obtenção de screenshot. Podemos criar, dentro da rule, um método que contenha a funcionalidade que estava no método com @After e chamá-lo sempre que um teste passar ou falhar. Perceba que, usando rules, podemos substituir facilmente métodos anteriormente anotados com @Before/@After. Nosso exemplo ficaria da seguinte forma:

public class ScreenshotRule extends TestWatcher  
{
   @Override
   protected void failed(Throwable e, Description description)
   {
       tiraScreenshot(description.getTestClass().getSimpleName(), description.getMethodName());

       after();
   }

   @Override
   protected void succeeded(Description description)
   {
      after();
   }

   public void after()
   {
      //Executa o mesmo que era executado no método com @After
   }

   public void tiraScreenshot(String nomeClasse, String nomeTeste)
   {
      //Criar diretório "screenshots", obter screenshot e criar arquivo .png
   }
}

No próximo post, vamos ver como implementar o mesmo recurso, mas utilizando o TestNG. Vocês vão ver como no TestNG é bem mais simples. ;)
Também vou publicar em breve um projeto de exemplo no meu GitHub mostrando a implementação tanto para o JUnit quanto para o TestNG. Postem qualquer dúvida/feedback nos comentários ou entrem em contato comigo.

Na seção de Referências abaixo, coloquei links explicando melhor como as rules funcionam, recomendo a leitura para quem quiser saber mais sobre.

Até o próximo post! :)


Referências

http://selenium.googlecode.com/git/docs/api/java/org/openqa/selenium/TakesScreenshot.html

WebDriver Advanced - RemoteWebDriver

http://junit-team.github.io/junit/javadoc/4.10/org/junit/rules/TestWatcher.html

http://www.threeriversinstitute.org/blog/?p=155

http://www.alexecollins.com/content/tutorial-junit-rule/

http://www.codeaffine.com/2012/09/24/junit-rules/


Sobre o autor: Stefan Teixeira trabalha como QA Engineer e, desde o final de 2014, tem se aventurado no mundo DevOps. É Bacharel em Ciência da Computação pela UFRJ e MBA em Garantia de Qualidade de Software pela Escola Politécnica da UFRJ. Entusiasta de Testes Automatizados (e de tudo que possa ser automatizado!), Agile Testing e da cultura DevOps.

Contatos: stefanfk@gmail.com | Twitter | LinkedIn


comments powered by Disqus