quinta-feira, 28 de fevereiro de 2008

Descobrindo o Selenium

Selenium é a ferramenta mais utilizada para Testes de Integração, ou ao menos a mais popular no meio Java.

Para os nossos exemplos, vamos usar o plugin para o FireFox que facilita muito a tarefa (Uma lista de links importantes esta no final do post, incluindo downloads de plugins e ferramentas).

O Selenium tem o seu próprio testRunner, porém nós vamos utilizar o Selenium-RC para testarmos via código Java, isso possibilita integrar os testes de Selenium com seus Testes Unitários.

Com o plugin do FireFox, iniciamos a usar o Selenium:



Começamos a gravar o processo que queremos testar, no caso um simples login:



Quando completamos o processo que queremos testar, paramos a gravação:



Exportamos o teste feito para código Java:



O código gerado é esse:

0: import com.thoughtworks.selenium.*;
1: import java.util.regex.Pattern;
2:
3: public class login extends SeleneseTestCase {
4: public void testLogin() throws Exception {
5: selenium.open("/login.action;" +
6: "jsessionid=21FC72563" +
7: "78F1D482601E23C929635A1");
8: selenium.type("j_password", "1234");
9: selenium.click("submit");
10: selenium.waitForPageToLoad("30000");
11: assertEquals("Área Inacessível",
12: selenium.getAlert());
13: }
14: }

O objeto selenium no método acima, nos da informações da página, como um alert, tempo esperado para a página carregar, o titulo, dentre outras coisas.

Para que o teste fique completo, estendemos TestCase, declaramos um atributo selenium do tipo Selenium e criamos o setUp e tearDown:

0: public void setUp() throws Exception {
1: String url =
2: "http://www.meuslivroseletronicos.com.br";
3: selenium = new DefaultSelenium("localhost",
4: SeleniumServer.getDefaultPort(),
5: "*firefox", url);
6: selenium.start();
7: }
8:
9: protected void tearDown() throws Exception {
10: selenium.stop();
11: }

A classe finalmente deve ficar assim:

0: import junit.framework.TestCase;
1: import org.openqa.selenium.server.SeleniumServer;
2: import com.thoughtworks.selenium.DefaultSelenium;
3: import com.thoughtworks.selenium.Selenium;
4:
5: public class login extends TestCase {
6: private Selenium selenium;
7: public void setUp() throws Exception {
8: String url = "http://www.meuslivroseletronicos" +
9: ".com.br";
10: selenium = new DefaultSelenium("localhost",
11: SeleniumServer.getDefaultPort(), "*firefox",
12: url);
13: selenium.start();
14: }
15:
16: protected void tearDown() throws Exception {
17: selenium.stop();
18: }
19:
20: public void testLoginLivro() throws Exception {
21: selenium.open("/login.action;jsessionid=BEF" +
22: "47FEE6B27A5E7EA20DB20E6F3F4F2");
23: selenium.type("j_username", "admin");
24: selenium.type("j_password", "1234");
25: selenium.click("submit");
26: selenium.waitForPageToLoad("30000");
27: assertEquals("Livro Eletronico",
28: selenium.getTitle());
29: }
30: }

Estarte o selenium server: java -jar selenium-server.jar que está no diretório selenium-remote-control-0.9.2\selenium-server-0.9.2 do Selenium-RC.

Agora rode como se fosse um teste comum do JUnit.



Links que podem ajudar:

Selenium (http://selenium-ide.openqa.org/)
Download em (http://selenium-core.openqa.org/download.jsp)

Selenium IDE 4 Firefox (http://release.openqa.org/selenium-ide/0.8.7/selenium-ide-0.8.7.xpi)

Selenium 4 Java (http://selenium-rc.openqa.org/java.html)
Download em (http://selenium-rc.openqa.org/download.jsp)

segunda-feira, 25 de fevereiro de 2008

Teste de Integração

O Teste de Integração, ao contrario do Teste Unitário, não se preocupa com o conceito de módulos. Essa modalidade de testes, como seu nome indica, promove o teste de uma funcionalidade sem se preocupar com "de quem é a responsabilidade?".

Pense numa situação de um cadastro, com Teste Unitário você deve criar um teste para as regras de negócios, um outro para os métodos de persistência, porém, no Teste de Integração, você testa somente o método Inserir, ou Update ou Delete e deixa que os módulos si relacionem de verdade, sem Mock Objects, sem nenhum artifício qualquer.

Quando falo "comparando" Testes Unitários com Testes de Integração, não estou dizendo que um é melhor que o outro, até porque eles devem conviver de maneira harmônica, os Testes de Integração verificam um outro escopo, verificam a integração entre os módulos, tanto que, se os testes unitários foram feitos de maneira correta, um teste de integração NUNCA (ao menos, não deverá) encontrará um problema na regra do método implementado, pode encontrar problemas numa injeção de dependências feita pelo Spring ou coisa parecida.

Breve vou escrever sobre o Selenium, uma ferramenta espetacular para fazer Testes de Integração.

segunda-feira, 18 de fevereiro de 2008

Melhorando o Design da Aplicação e a construção de testes

O TDD tem como sua maior incumbência, auxiliar na construção do design dos seus métodos, com o tempo vimos situações particulares e tentamos encontrar soluções para as mais diversas situações, como JMockit, CGLib e outros.

Porém tivemos, aqui na empresa, a visita do Paulo Silveira da CAELUM e mudou bastante o meu conceito de como alcançar a melhoria no desenvolvimento de testes e consequentemente, das classes de um sistema.

Citarei umas idéias que achei bem interessantes:

1) Use Injeção de Dependência. No Spring temos o IoC que nos proporciona essa técnica, é notório a facilidade que ganhamos na construção de testes, principalmente no TDD.
2) Use e abuse de Interfaces. O uso de Interfaces nos oferece, além do óbvio, uma facilidade muito maior na construção de testes, pois o padrão para construção de Mock Objects é utilizar Interfaces.
3) Evite os método estáticos. Métodos estáticos tem alto acoplamento e são ruins de testar, neste momento eu falei sobre JMockit, mas ele frisou que testes devem ser simples e que artifícios como esses podem complicá-los.

Por fim, ele falou algo que achei bem interessante, acho que ele tirou do Design Patters do GoF:

"Evite as extenções e abuse das implementações", não sei bem se foram essas as palavras, mas a idéia foi essa.

E como me lembrou o Capitão Sávio Pierre, "Em vez de extender, procure fazer Agregações ou Composições". Isso também é sensacional.

Bem, acho que existem situações em que essas boas práticas podem não ser possíveis, mas confesso que estarei sempre atento a elas.

Fica aqui a indicação da CAELUM e do próprio Paulo Silveira, o workshop ministrado por ele foi muito bacana.

segunda-feira, 11 de fevereiro de 2008

Fazendo testes com Ant + JUnit + JMockit

Para fazer seus testes com o auxílio do Ant, seguem alguns passos que extraí do que já utilizamos aqui na empresa:

Primeiro Passo: crie 2 arquivos e coloque na raiz da aplicação:

build.xml
build.properties


Segundo Passo: no build.xml, crie a seguinte estrutura (ela pode ser mais simplificada):

*tire os '$' de todas as linhas
<$project name="NomeDoProjeto" basedir=".." default="integrate">
<$property file="${ant.project.name}/build.properties"/>
<$property name="project.dir" value="${ant.project.name}"/>
<$property name="src.dir" value="${project.dir}/src"/>
<$property name="build.dir" value="${project.dir}/web/WEB-INF/classes"/>
<$property name="lib.dir" value="${project.dir}/web/WEB-INF/lib"/>
<$property name="allTests.class" value="com.NomeDoProjeto.test.AllTests"/>

<$path id="project.classpath">
<$pathelement location="${build.dir}"/>
<$fileset dir="${lib.dir}">
<$include name="*.jar"/>
<$/fileset>
<$fileset dir="${tomcat.lib}">
<$include name="*-api.jar"/>
<$include name="jasper-runtime.jar"/>
<$/fileset>
<$/path>

<$taskdef name="junit" classname="org.apache.tools.ant.taskdefs.optional.junit.JUnitTask">
<$classpath refid="project.classpath"/>
<$/taskdef>

<$target name="build">
<$mkdir dir="${build.dir}"/>
<$javac debug="on" srcdir="${src.dir}" destdir="${build.dir}" failonerror="true">
<$classpath refid="project.classpath"/>
<$/javac>
<$/target>

<$target name="test" depends="build">
<$dirname file="build.xml" property="current.dir"/>
<$junit haltonfailure="true" haltonerror="true" fork="true" dir="${current.dir}">
<$jvmarg value="-javaagent:${jmockit.dir}"/>
<$classpath>
<$path refid="project.classpath"/>
<$/classpath>
<$formatter type="plain" usefile="false"/>
<$test name="${allTests.class}"/>
<$/junit>
<$/target>

<$/project>

Terceiro Passo: no build.properties, crie as variáveis( ${variavel} ) que são usadas no xml:

tomcat.lib=C:\\Arquivos de programas\\Apache Software Foundation\\Tomcat 5.5\\common\\lib
jmockit.dir=C:\\jmockit.jar

Quarto Passo: Para rodar o Ant (estou usando o Eclipse), clique com o botão direito no build.xml / Run As / Ant Build...

Na aba Targets, marque a target test e na aba Classpath, adicione o jar do JUnit.



A partir daí é só rodar.


Para o uso do JMockit adicionamos estas linhas (estão adicionadas nos exemplos acima, as linhas seguintes são somente informativas, porém se você não usar JMockit, remova-as):

No build.xml:
*sem o '$'
<$jvmarg value="-javaagent:${jmockit.dir}"/>
Adiciona o argumento na JVM.

No build.properties:
jmockit.dir=C:\\jmockit.jar
Indica onde está o jar do JMockit.

Problema com JMockit

O JMockit foi comentado no post Mockando um Método Estático.

Bem, em algumas maquinas aqui da empresa, estava acontecendo o seguinte erro:

java.lang.VerifyError: (class: junit/framework/TestCase, method: runTest signature: ()V) Illegal constant pool index
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Class.java:2365)
at java.lang.Class.getMethod0(Class.java:2611)
at java.lang.Class.getMethod(Class.java:1579)at org.eclipse.jdt.internal.junit.runner.junit3.JUnit3TestLoader.
getTest(JUnit3TestLoader.java:99)at org.eclipse.jdt.internal.junit.runner.junit3.JUnit3TestLoader.
loadTests(JUnit3TestLoader.java:59)at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.
runTests(RemoteTestRunner.java:445)at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.
runTests(RemoteTestRunner.java:673)at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.
run(RemoteTestRunner.java:386)at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.
main(RemoteTestRunner.java:196)



O interessante esta no trecho "em algumas maquinas", se o jar do JMockit e o método onde foi usado são os mesmos, por que somente em algumas maquinas aconteceu esse erro, e não em todas?

A verdade é que o erro não tem nada haver com JMockit, mas como faz parte do processo de fazer o JMockit funcionar, vou postar a minha solução aqui:

Li em vários lugares que o problema era causado pela JVM, que não suportava o parâmetro (figura abaixo). Onde li, dizia que o motivo era a quantidade de parâmetros passados pra JVM, e que era um problema da VM do Java 5. Não me aprofundei no assunto e nem pretendo.



O Runtime JRE, estava apontando para o diretório de uma JRE instalada, então apontei para o diretório da JDK e pronto, funcionou:



Caso este procedimento não seja suficiente para fazer com que seus testes funcionem, o problema pode ser da versão da sua JVM (a testada foi 1.5.0_08), instale outra e refaça o procedimento.

quinta-feira, 7 de fevereiro de 2008

Confusões sobre Cobertura de Código

Existem muitas idéias confusas sobre o conceito "100% de Cobertura", além das que já existentes no sentido de seguir ou não esta prática.

Começo esclarecendo que a Cobertura de Código é baseada em código coberto por Testes Unitários, pois um teste de Integração, por exemplo, testa telas, integração entre módulos, mas não analisa linhas cobertas.

Outra confusão é sobre a profundidade de um teste, quando tratamos deste assunto. Para que um método esteja 100% Coberto, o teste precisa somente passar por todas as suas linhas do código do método testado. Desta forma, o teste pode não ser completo, pois não testará "todas" as possibilidades, e mesmo assim será considerado 100% coberto. Com isso, não quero dizer que não devemos procurar a completude do teste.

A cobertura de código não garante, por si só, a qualidade no desenvolvimento, ter seu código 100% coberto é somente uma das várias práticas que lhe ajudam a obter sucesso no desenvolvimento.

Para garantir 100% de Cobertura, algumas vezes você será obrigado a fazer testes "inúteis", tomando o exemplo de meros getters e setters, esse é um dos motivos que faz com que algumas pessoas não "gostem" da idéia.

Manter seu código 100% Coberto é uma tarefa muito questionada, em várias situações quando nos preocupamos apenas com a arquitetura da nossa aplicação, preferimos não manter o código 100% Coberto. Porém, essa decisão tem efeitos comerciais, e estes também devem ser considerados.