Utilitários de teste 2
E-Plamtax foi um projeto muito positivo que executamos para a Força Aérea. A equipe do E-Plamtax aprendeu muitas coisas no projeto, mas muitas dessas aprendizagens ficaram guardadas conosco. Uma das mais interessantes é a criação de utilitários de teste.
Utilitários de teste são uma maneira de reaproveitar código em testes unitários. Usualmente, isso é feito utilizando os métodos setUp ou @Before dos casos de teste, mas isso tem algumas desvantagens. Por exemplo, em um caso de teste, podemos ter a seguinte inicialização:
private Address address;
private AddressDAO addressDAO;
@Before
public void setUp() {
address = new Address();
address.setStreet("Rua fulano");
address.setNumber("123/A");
addressDAO = new AddressDAO();
}
Se tivermos um teste com o abaixo essa inicialização funciona bem…
@Test
public void testGetAllAddresses(){
addressDAO.addAddress(address);
List<Address> addresses = addressDAO.getAllAddresses();
assertEquals(1, addresses.size());
assertEquals("Rua fulano", addresses.get(0).getStreet());
assertEquals("123/A", addresses.get(0).getNumber());
}
Agora, se tivermos o teste abaixo, o objeto criado é desperdiçado:
@Test
public void testGetNoAddress() {
List<Address> addresses = addressDAO.getAllAddresses();
assertEquals(0, addresses.size());
}
Se o código for como o seguinte, teremos redundância de código. também temos de decidir SE o outro objeto deve ser criado no @Before também ou no método.
@Test
public void testGetAllAddressesMoreThanOne() {
addressDAO.addAddress(address);
Address address2 = new Address();
address2.setStreet("Outra rua");
address2.setNumber("111");
addressDAO.addAddress(address2);
List<Address> addresses = addressDAO.getAllAddresses();
assertEquals(1, addresses.size());
assertEquals("Rua fulano", addresses.get(0).getStreet());
assertEquals("123/A", addresses.get(0).getNumber());
}
Esses inconvenientes são menores quando comparados à tarefa de criar as dependências e um objeto para o teste. Por exemplo, para testar uma classe Person que agrega um Address em um outro caso de teste, teremos de ter um @Before semelhante a esse:
private Person person;
private Address address;
private PersonDAO personDAO;
@Before
public void setUp() {
address = new Address();
address.setStreet("Rua fulano");
address.setNumber("123/A");
person = new Person();
person.setName("João");
person.setAddress(address);
personDAO = new PersonDAO();
}
O código para a criação de endereços replicou-se, e é difícil criar as dependências. Nesses exemplos, vemos casos simples, mas é fácil visualizar como a situação irá se complicar.
Nós solucionamos esse problema criando uma classe para criar esses objetos. Essa classe seria algo como isso:
public class TestUtil {
public static Address utilCreateAddress(String street, String number) {
Address address = new Address();
address.setStreet("Rua fulano");
address.setNumber("123/A");
return address;
}
public static Person utilCreatePerson(String name, Address address) {
Person person = new Person();
person.setName(name);
person.setAddress(address);
return person;
}
}
Nossos casos de teste estendiam a TestUtil, facilitando a criação de objetos:
public class TestAddress2 extends TestUtil {
private AddressDAO addressDAO = new AddressDAO();
@Test
public void testGetAllAddresses() {
Address address = utilCreateAddress("Rua fulano", "123/A");
addressDAO.addAddress(address);
List<Address> addresses = addressDAO.getAllAddresses();
assertEquals(1, addresses.size());
assertEquals("Rua fulano", addresses.get(0).getStreet());
assertEquals("123/A", addresses.get(0).getNumber());
}
@Test
public void testGetNoAddress() {
List<Address> addresses = addressDAO.getAllAddresses();
assertEquals(0, addresses.size());
}
@Test
public void testGetAllAddressesMoreThanOne() {
Address address = utilCreateAddress("Rua fulano", "123/A");
Address address2 = utilCreateAddress("Outra rua", "111");
addressDAO.addAddress(address);
addressDAO.addAddress(address2);
List<Address> addresses = addressDAO.getAllAddresses();
assertEquals(2, addresses.size());
assertEquals("Rua fulano", addresses.get(0).getStreet());
assertEquals("123/A", addresses.get(0).getNumber());
}
}
Como também precisávamos frequentemente de um objeto qualquer, ou que apenas um ou outro parâmetro fosse definido, criávamos variantes dos métodos:
public static Address utilCreateAddress() {
return utilCreateAddress("Qualquer", "Qualquer");
}
public static Person utilCreatePerson() {
return utilCreatePerson("José", utilCreateAddress());
}
O E-Plamtax foi um projeto um tanto complexo, com grandes redes de dependências de objetos. O uso desses utilitários de teste viabilizou a prática de TDD no sistema. Era emocionante descobrir que, para criar aquele documento que dependia de sete outros documentos e uns cinco ou seis usuários, bastava chamar um método :)
Naturalmente, há mais sobre nossos utilitários de teste do que foi escrito aqui, e pode haver mais ainda que sequer fizemos. (Por exemplo, pode ser interessante produzir utilitários de teste para classes específicas, ao invés de um gigantesco utilitário) Entretanto, como a ideia é bem simples, esperamos que esse pontapé inicial lhe motive a pensar sobre o tema. Até mais!
Selenium e XPath 6
Quem já acompanha o blog deve ter percebido que usamos o Selenium. Primariamente, terminávamos versões preliminares da interface e usávamos o Selenium IDE para gerar scripts de testes.
Todavia, essa abordagem tinha complicações. Mudanças de design quebravam os testes. Para repará-los tínhamos de percorrer todos os caminhos através do Selenium IDE. Isso tomava tanto tempo que os testes de Selenium acabaram abandonados.
Depois de surpresas com algumas telas, retomamos os testes. Entretanto, estamos usando outra abordagem. Para compreendê-la, é preciso conhecer XPath.
XPath
XPath é uma pequena linguagem para selecionar elementos, atributos, textos etc. de um documento XML. Considere, por exemplo, o documento XML abaixo:
<library>
<book id="war-peace">
<title>War and Peace</title>
<edition>1</edition>
<author>Leon Tolstoi</author>
</book>
<book id="devil-backcountry">
<title>The Devil Pays in the Backcountry</title>
<edition>2</edition>
<author>Guimarães Rosa</author>
</book>
<book id="art1">
<title>The Art of Computer Programming</title>
<edition>1</edition>
<volume>1</volume>
<author>Donald Knuth</author>
</book>
<book id="mythical">
<title>The Mythical Man-month</title>
<edition>2</edition>
<author>Fred Brooks</author>
</book>
</library>
A expressão XPath /library/book[2] retorna o elemento
<book id="devil-backcountry">
<title>The Devil Pays in the Backcountry</title>
<edition>2</edition>
<author>Guimarães Rosa</author>
</book>
Já expressão /library/book[@id='mythical'] retorna o elemento:
<book id="mythical">
<title>The Mythical Man-month</title>
<edition>2</edition>
<author>Fred Brooks</author>
</book>
e a expressão /library/book[@id='mythical']/author/text() retorna
fred Brooks
Esses exemplos são só para instigar sua curiosidade. Para aprender mais sobre XPath, recomendamos o tutorial da W3Schools. O add-on XPath Checker do Firefox também é uma mão na roda. Se você utiliza Linux, ferramentas como o xgrep e xmlstarlet podem ajudar nos estudos e permitir scripts sofisticados.
Dando Nome aos Bois
Usando o Selenium IDE, nossos scripts pareciam com isso:
selenium.open("/admin/texts");
selenium.type("article_title", "Selenium e XPath");
selenium.click("Salvar");
selenium.waitForPageToLoad("30000");
selenium.click("Artigos");
selenium.waitForPageToLoad("30000");
selenium.click("link=Retornar");
assertFalse(selenium.isTextPresent("Erro"));
Porém, o cliente pedia para mudar coisas. “Artigos” passariam a ser ”posts”, “Retornar” viraria “Voltar”, e “Salvar” seria agora “Gravar”. O link inicial não seria mais /admin/texts, mas sim /administracao/conteudo. Haveria um texto de ajuda no final, explicando que “Erros podem ser problemas de login” - o que faria com que o texto “Erro” estivesse presente em todas as páginas.
Para evitar eses problemas, demos nomes (ou ids) a todos os links, input, div, td etc. Também passamos a utilizar menos o Selenium IDE, e a digitar caminhos XPath explícitos. Por exemplo, se o link referenciado por link=Artigos fosse gerado por:
<a href="artigos/listar.html">Artigos</a>
nós adicionaríamos um id ao link
<a id="listarArtigos" href="artigos/listar.html">Artigos</a>
e, no lugar de
selenium.click("link=Artigos");
utilizamos
selenium.click("//a[@id='listarArtigos']");
//a[@id='listar_artigos'] é o caminho XPath para o link, que agora é independente do texto apresentado e inequivocamente encontrável. O mesmo vale para os campos de formulários. Por exemplo, o botão submit referenciardo por Salvar seria algo como
<input type="submit" value="Salvar" />
Depois da “reforma”, nós adicionamos um atributo name ao submit:
<input name="salvarArtigo" type="submit" value="Salvar" />
Agora, poderemos referenciá-lo através do nome, que é independente da apresentação:
selenium.click("//input[@name='salvarArtigos']");
Segundo o novo padrão, nosso teste será algo como
selenium.open("/admin/texts");
selenium.type("//input[@name='artigo.titulo']", "Selenium e XPath");
selenium.click("//input[@name='salvarArtigos']");
selenium.waitForPageToLoad("30000");
selenium.click("//a[@id='listarArtigos']");
selenium.waitForPageToLoad("30000");
selenium.click("//a[@id='retornar']");
assertTrue(selenium.isTextPresent("//div[@id='Erro']"));
Ao final, note como o selenium.isTextPresent() também verifica a presença de um elemento, não de um texto.
Esse processo, embora exija um pouco mais de trabalho, permite gerar códigos mais robustos, fáceis de manter e reutilizáveis.