quinta-feira, 24 de janeiro de 2008

Teste Unitário - Como definir o seu escopo?

Quando se está começando a fazer testes, o assunto mais incompreendido é "até onde o teste unitário deve testar". Muitos confundem a idéia de unidade, e até começar a usar Mock Objects é um passo gigantesco e confuso.

Definições encontradas na WEB:

"O teste unitário ou de unidade é uma modalidade de testes que se concentra na verificação da menor unidade do projeto de software."
"Este estágio tem foco na verificação dos menores elementos testáveis do software."
"O teste unitário se limita a testar uma única unidade isolada."

Todas estão certas, e a minha definição para essa categoria de testes é: "Um teste que não ultrapassa a responsabilidade do método testado".

O que quer dizer que, não podemos ultrapassar a responsabilidade do método, da classe ou da camada. O negócio exige bom senso, pois um exemplo de quando ultrapassar a barreira do método é no caso de testar Métodos Privados, porém os escopos se confundem nesses casos.

Situações mais claras são quando tratamos classes diferentes, por exemplo:

Temos uma classe (ClassePeba) e testamos um método (getAlgo) desta classe:

 0: public class ClassePeba {
1: public String getAlgo(int i){
2: String palavra = "blablabla";
3: ClasseCriptografa classeCriptografa =
4: new ClasseCriptografa();
5: palavra = classeCriptografa.criptogravaPalavra(
6: palavra, i);
7: return palavra;
8: }
9: }
o método (getAlgo) invoca um outro método (criptogravaPalavra) de outra classe (ClasseCriptografa):

 0: public class ClasseCriptografa {
1: public String criptogravaPalavra(String palavra, int i) {
2: if(i == 1)
3: {
4: palavra = palavra.replace("a", "@");
5: palavra = palavra.replace("b", "!");
6: palavra = palavra.replace("l", "?");
7: }else{
8: palavra = palavra.replace("a", "*");
9: palavra = palavra.replace("b", "$");
10: palavra = palavra.replace("l", "%");
11: }
12:
13: return palavra;
14: }
15: }

No teste do método (getAlgo):

 0: import org.junit.Before;
1: import org.junit.Test;
2: import static org.junit.Assert.*;
3:
4: public class TestClassePeba {
5:
6: ClassePeba classePeba;
7:
8: @Before
9: public void prepare()
10: {
11: classePeba = new ClassePeba();
12: }
13:
14: @Test
15: public void testGetAlgo()
16: {
17: assertEquals("!?@!?@!?@", classePeba.getAlgo(1));
18: assertEquals("$%*$%*$%*", classePeba.getAlgo(2));
19: }
20: }

Estamos testando duas classes distintas, caso aconteça uma inconformidade na outra classe (ClasseCriptografa), o erro irá acontecer no teste do método getAlgo, que não tem erro algum.

Dessa forma, o que deve ser feito é:

Primeiro, faço uma Refatoração para não instanciar a classe dentro do método, o teste definiu o design do método:
0: public String getAlgo(int i,
1: ClasseCriptografa classeCriptografa){
2:
3: String palavra = "blablabla";
4: palavra = classeCriptografa.criptogravaPalavra(
5: palavra, i);
6: return palavra;
7: }
Crie um teste para o método getAlgo, mockando a classe ClasseCriptografa:

* para mockar uma classe com JMock, é necessário** criar a interface:
0: public interface ClasseCriptografiaInterface {
1: public abstract String criptogravaPalavra(
2: String palavra, int i);
3: }
** tambem é possível mockar uma classe concreta, veja aqui.

 0: import org.jmock.Mock;
1: import org.jmock.MockObjectTestCase;
2: import org.junit.Before;
3: import org.junit.Test;
4:
5: public class TestClassePeba extends MockObjectTestCase{
6:
7: ClassePeba classePeba;
8: Mock classeCriptografa = null;
9:
10: @Before
11: public void setUp()
12: {
13: classePeba = new ClassePeba();
14: classeCriptografa = new Mock(
15: ClasseCriptografiaInterface.class);
16: }
17:
18: @Test
19: public void testGetAlgo()
20: {
21: classeCriptografa.expects(once()).
22: method("criptogravaPalavra").
23: with(eq("blablabla"), eq(1)).will(returnValue(
24: "!?@!?@!?@"));
25: assertEquals("!?@!?@!?@", classePeba.getAlgo(1, (
26: ClasseCriptografiaInterface)classeCriptografa.
27: proxy()));
28:
29: classeCriptografa.expects(once()).method(
30: "criptogravaPalavra").with(eq("blablabla"),
31: eq(2)).will(returnValue("$%*$%*$%*"));
32: assertEquals("$%*$%*$%*", classePeba.getAlgo(2,
33: (ClasseCriptografiaInterface)classeCriptografa.
34: proxy()));
35: }
36: }

Até aqui o método criptogravaPalavra não foi sequer executado.

Depois criamos um teste específico para a classe ClasseCriptografa:

 0: import static org.junit.Assert.assertEquals;
1:
2: import org.junit.Before;
3: import org.junit.Test;
4:
5: public class TestClasseCriptografa{
6:
7: ClasseCriptografa classeCriptografa;
8:
9: @Before
10: public void setUp()
11: {
12: classeCriptografa = new ClasseCriptografa();
13: }
14:
15: @Test
16: public void testGetAlgo()
17: {
18: assertEquals("!?@!?@!?@", classeCriptografa.
19: criptogravaPalavra("blablabla",1));
20: assertEquals("$%*$%*$%*", classeCriptografa.
21: criptogravaPalavra("blablabla",2));
22: }
23: }

Pronto!

A idéia é ter seus testes bem separados, pois em momentos de refatoração, encontrar os erros do método e a alteração do próprio teste será facilitada.

terça-feira, 22 de janeiro de 2008

TestNG - Uma ótima alternativa

O TestNG é uma ferramenta largamente utilizada para fazer Testes de Unidade (pelo que andei ouvindo, até mais que isso), aqui na empresa, o Israel Freitas usa essa ferramenta e tenta a muito tempo evangelizar outros, porém pouco lhe dei ouvidos, "já que eu tenho o JUnit pra que procurar outra ferramenta que faz a mesma coisa de maneira diferente" ? Mas por insistência dele, resolvi fazer um teste e publicar algo sobre o assunto, confesso que minha opinião sobre o TestNG mudou de maneira drástica.


Esta ferramenta foi criada por Cédric Beust (a senior software engineer at Google) e Alexandru Popescu, esta já usa conceitos como Annotations bem antes do JUnit, além disso, percebi muitas funcionalidades e facilidades oferecidas por ele.

Um conceito interessante é o de dependência entre testes, vale a pena ser estudado.

O TestNG (a versão testada é a 5.6) permite a utilização dos asserts do JUnit, inserindo somente o import: import static org.testng.AssertJUnit.*;

Para ilustrar, criei um exemplo bem simples:


Foi criado a classe POJO somente com 2 atributos e seus getters e setters:

 0: public class POJO {
1:
2: private int id;
3: private String nome;
4:
5: public int getId() {
6: return id;
7: }
8: public void setId(int id) {
9: this.id = id;
10: }
11: public String getNome() {
12: return nome;
13: }
14: public void setNome(String nome) {
15: this.nome = nome;
16: }
17: }

Um teste bem simples foi feito pra a classe:

 0: import org.testng.annotations.*;
1: import static org.testng.AssertJUnit.*;
2:
3: public class POJOTest{
4:
5: @BeforeClass
6: public void setUp() {
7: }
8:
9: @Test
10: public void gettersAndSettersTest()
11: {
12: POJO pojo = getEntidade();
13: assertEquals(pojo.getId(), 1);
14: }
15:
16: private POJO getEntidade() {
17: POJO pojo = new POJO();
18: pojo.setId(1);
19: pojo.setNome("nome");
20: return pojo;
21: }
22: }

* Perceba que uso os asserts do JUnit.

Criei o arquivo de configuração do TestNG:
*tire os '$' de todas as linhas

<$!DOCTYPE suite SYSTEM "http://beust.com/testng/testng-1.0.dtd" >
<$suite name="My First TestNG test">
<$test name="Hello Test!">
<$classes>
<$class name="POJOTest" />
<$/classes>
<$/test>
<$/suite>


Utilizando o plugin para o Eclipse rodei este teste com extrema facilidade e o log do resultado foi gerado:

[Parser] Running:
C:\Arquivos de programas\Apache Software Foundation\Tomcat 5.5\webapps\TestNGExamples\temp-testng-customsuite.xml

PASSED: gettersAndSettersTest

===============================================
POJOTest
Tests run: 1, Failures: 0, Skips: 0
===============================================


===============================================
TestNGExamples
Total tests run: 1, Failures: 0, Skips: 0
===============================================

O nível de detalhes do log do resultado é determinado na propriedade "Log Level" da configuração da execução do TestNG.


Os links abaixo vão ajudar muito na utilização do TestNG.

Site oficial
Plugin pro Eclipse
Annotations pra todos os gostos
TestNG com Ant
Automatizando a Migração do JUnit para o TestNG
Comparação entre TestNG e JUnit4
Bom artigo sobre o assunto


Avaliação geral:

Como um usuário do JUnit, considero o TestNG um adversário de peso, bem mais rico quando se fala de annotations. Acredito que esta ferramenta deve ter seu uso considerado, além de ser muito robusta e cheia de novidades, tem muita documentação disponível na WEB.
A única carência que percebi, foi a ausência de asserts, porém com introdução dos asserts do JUnit a carência foi resolvida.

sexta-feira, 18 de janeiro de 2008

Teste de Regressão

Sempre escuto falar nesse tal, e achando que não teria tempo para estudar mais uma "novidade" no mundo dos testes, fui adiando, adiando até a curiosidade me forçar a procurar algo sobre o assunto. Daí vi essa definição:

"O Teste de Regressão consiste na aplicação de testes à versão mais recente do software, para garantir que não surgiram novos defeitos em componentes já testados. Se ao juntar o novo componente ou as suas alterações com os restantes componentes do sistema, surgirem novos defeitos em componentes inalterados, então considera-se que o sistema regrediu."

Vi este outro conceito também:

"Os testes automáticos têm uma vantagem em relação aos manuais: são facilmente repetíveis. Portanto, são favorecidos no momento dos testes de regressão."

Dessa forma, percebi que faço testes de regressão várias vezes ao dia, sempre que pratico Integração Contínua, percebi também a força deste conceito e que é muito fácil ter Testes de Regressão, basta manter os testes criados no TDD e a Integração Contínua acontecendo de maneira sistemática.

Uma dica para quem está começando é: Não faça esses testes de forma manual, automatize.

quarta-feira, 16 de janeiro de 2008

Passagem de vários parâmetros para método mockado

De acordo com o post Passagem de parâmetros para método mockado, a quantidade de parâmetros passados para o método mockado deve ser igual a do método "de verdade", desta forma encontrei um problema esta manhã:

Tinha um teste para um método, e nele, mockava um método de um outro objeto da seguinte forma:

historicoClienteDao.expects(once()).method("findByFiltros").with(1L, "nome", null, 3L).will(returnValue(historicos));

porém, este método ganhou mais um parâmetro e quando o adicionei, deu erro de compilação:

historicoClienteDao.expects(once()).method("findByFiltros").with(1L, "nome", null, 3L, true).will(returnValue(historicos));

pois o método with só recebe 4 Constraints.

Aí foi que percebi que ele também recebe um array de Constraints e então o "problema" foi resolvido da seguinte forma:

historicoClienteDao.expects(once()).method("findByFiltros").with(new Constraint[]{1L, "nome", null, 3L, true}).will(returnValue(historicos));


ps. a ferramenta usada neste exemplo é o JMock.

domingo, 13 de janeiro de 2008

TDD - Os testes devem ser feitos antes ou durante a implementação?

A resposta é clara, DURANTE. Porém, existem vertentes que tratam o "DURANTE" de maneira mais ou menos radical, bem vou tentar explicar o conceito:

A vertente 1 acredita que o código de testes deve ser feito "ao mesmo" tempo que a implementação da funcionalidade. Bem, sejamos mais claros, os praticantes dessa vertente acreditam que deve ir descobrindo o que o método vai fazer a medida que o vai escrevendo.

Nessa abordagem, o FeedBack é mais rápido.

leitura recomendada

A vertente 2 considera a possibilidade de começar "ANTES" e continuar "DURANTE".

Num comentário do Thiago Arrais sobre a leitura recomendada acima, a idéia de fazer o teste "DURANTE" a implementação tem um conceito menos literal, veja a frase: "Escreva os testes que conseguir imaginar antes de começar a escrever o código...".

Como o próprio Thiago se preocupa em "não demorar demais pensando", esta técnica deve ter este cuidado tomado, então concluímos essa abordagem como:

Faça os testes que você idealizou imediatamente, como num validador de PDF, essa técnica diz, por exemplo, que você já pode fazer os testes pra dois CPFs diferentes(um certo e um errado), antes mesmo de implementar a primeira linha do código do validador, você também pode testar um CPF com menos dígitos, com mais dígitos, etc.

Nessa abordagem, a maior vantagem é "pensar mais" antes de começar a implementação.

Eu sou um seguidor da segunda vertente, isso porque considero mais fácil de fazer e obtive melhores resultados fazendo dessa forma, porém não sou nada radical quanto a isso, acredito que existam várias situações em que a primeira vertente seja mais indicada. Também não acho que exista a "forma certa" de fazer, o certo é que vale a pena ser feito.

TDD - Testes são somente a consequência

Pois é, você pode até ter ouvido isso em outro lugar, mas existe uma grande chance de você ter "descoberto" antes mesmo de ouvir.

Sei que é óbvio, mas como vejo muita gente confundindo o "processo" de TDD com Testes Unitários, resolvi dar minha contribuição.

O TDD tem como objetivo dirigir o design da implementação, essa é, sem dúvida, o maior lucro de usar TDD. O segundo maior lucro é ter sua aplicação coberta por testes.

Tendo suas implementações dirigidas por testes, estes se tornam parte da sua aplicação, pra ser mais claro, o código desenvolvido na classe de teste passa a funcionar "até mesmo" como documentação, além do óbvio, que é o fato da sua implementação ter sido baseada no teste. Um método pode, em muitas situações, ter sua implementação modificada pra facilitar o teste.

Por que a confusão(TDD X Testes Unitários) acontece?

Simples, porque fazer TDD é fazer Teste Unitário. Porém, você pode fazer o teste unitário após a funcionalidade estar concluída e isso não é TDD, você terá seu código 100% coberto, mas como já falei, o beneficio maior é dirigir a implementação, ter seu código 100% coberto é apenas a consequência de fazer TDD.

quinta-feira, 10 de janeiro de 2008

Testando Actions do WebWork 2

No site da opensymphony tem um artigo mostrando como Testar Actions do WW, como não entendi direito, pedi um auxílio ao Igo Coelho e vi que o negócio é mais simples do que parece.

Para testar Actions, não precisamos estender nada, porém temos que ficar atentos com as variáveis da classe, pois essas terão que ser informadas.

1) Pra começar, crie uma instancia da Action a ser testada:

private ArtigoEditAction action;

é essa variável que será utilizada para invocar os métodos, como também, setar as variáveis da Action.

2) setUp e tearDown pra deixar as coisas nos seus lugares:
 0: protected void setUp() throws Exception
1: {
2: super.setUp();
3: action = new ArtigoEditAction();
4: }
5:
6: protected void tearDown() throws Exception
7: {
8: action = null;
9: super.tearDown();
10: }
3) A grande maioria desses testes consiste apenas em verificar o direcionamento:
0: public void testExecute() throws Exception
1: {
2: assertEquals(action.execute(), "success");
3: }
4) Como as Actions não retornam os objetos que usaremos nas páginas, eles são populados e passados por request (eu acho), é importante verificar o estado da variável:

para o método:
0: public String listNormas() throws Exception
1: {
2: artigo = artigoManager.findById(artigo.getId());
3: return Action.SUCCESS;
4: }
a variável artigo é alterada, então não devemos testar somente o retorno, devemos testar também o estado dessa variável após o método ser executado:
 0: public void testList() throws Exception
1: {
2: Artigo artigo = new Artigo();
3: artigo.setId(1L);
4:
5: Artigo artigoCheio = new Artigo();
6: artigoCheio.setId(1L);
7: artigoCheio.setResumo("resumo");
8:
9: action.setArtigo(artigo);
10:
11: artigoManager.expects(once()).method("findById").
12: with(eq(artigo.getId())).will(returnValue(artigoCheio));
13:
14: assertEquals(action.getArtigo().getResumo(), null);
15: assertEquals(action.listNormas(), "success");
16: assertEquals(action.getArtigo().getResumo(), "resumo");
17: }
5) Para efeito de 100% de cobertura, eventualmente você terá que forçar umas chamadas de get ou set de alguma propriedade, pois é bem possível que esta não seja invocada naturalmente:
* Um plugin como o eclEmma pode te ajudar muito nessas horas.
 0: public void testCobrirGetsSets()
1: {
2: Collection vendedores = new ArrayList();
3: action.setVendedores(vendedores);
4: action.getDataFinalVendas();
5: action.getProdutoPacotes();
6: action.getProdutoPacote();
7: action.getDataInicioVendas();
8: action.getVendedor();
9: }
* sei que é horrível, mas é melhor um teste feio do que não ter o teste.


Só isso, fácil fazer teste para as Actions, né!? :P Se encontrar alguma dificuldade postarei aqui, mas acabou a desculpa de não fazer testes nas Actions.

quarta-feira, 9 de janeiro de 2008

Boas práticas - Parte I

Hoje, desenvolvendo algumas funcionalidades que faziam consultas ao BD, percebi que existem algumas técnicas que podem facilitar a implementação e fortalecer os testes, por isso decidi criar a sessão Boas práticas.

Neste post vou falar sobre boas práticas em testes para consultas.

Se você estiver usando SQL "puro" talvez não tenha tanto problema (mesmo achando que essas boas práticas devem ser seguidas) mas usando APIs de persistência, como Hibernate, essas práticas se fazem quase obrigatórias:

O caso de uso é trazer todas as operações ocorridas entre duas data (os dias 10 e 20 de dezembro, por exemplo).

* crie testes com valores nos limites.
crie registros ocorridos exatamente em cada uma das datas (10 e 20 de dezembro).

* crie testes com valores intermediários.
crie um registro, pelo menos, ocorrido numa data intermediária (dia 15, por exemplo).

* crie testes com valores imediatamente fora dos intervalos.
crie registros ocorridos nas datas vizinhas as limites(9 e 21 de dezembro).

* se houverem mais campos condicionais, como por exemplo "Tipo de Operação", crie registros que "contrariem" somente essa condição.
isso é muito útil em sistemas multi-empresa.


Lógico que existem várias outras boas práticas (por isso postei no título: Parte I), e sempre que lembrar vou tentar postá-las aqui.

ps. Se você tem mais boas práticas e quiser vê-las neste blog, óbvio que eu preciso concordar com elas, me envie por email (esta lá em "Quem sou?").

terça-feira, 8 de janeiro de 2008

Testes dirigem o design

Pois é, esses dias tive uma discussão na xprio sobre um teste que estava bem complicado, isso contraria várias idéias de que os testes não devem ser tão complicados de implementar, o que é verdade, eles não devem ser complicados, bem, o caso era o seguinte:

Estava testando um método:
0: public void enviaEmail()
1: {
2: // faz um monte de coisa
3: Mail mail = new Mail();
4: mail.send("Titulo", "Corpo", "email@email.com.br");
5:
6: // faz outro monte de coisa
7: }
Dentro deste método, crio a instância de outra classe(Mail) e invoco o método(send) desta. Aí esta o problema, este método(send) envia emails, e isso é algo que não preciso que meu teste faça.

O Danilo Sato levantou esta bola(o teste dirige o design), e me fez algumas perguntas:

* Que tal passar o e-mail pronto como parâmetro?
* Vale a pena ter o e-mail como atributo dessa classe?
* Qual a responsabilidade dessa classe que está testando? enviar o
e-mail ou criar o email?

Essas perguntas podem ser generalizadas e usadas em situações onde o teste se torne mais complicado.

Como se trata de uma situação onde o código já existe e funciona, fazer uma refatoração é o mais indicado a fazer, nada de grandes manobras, apenas desacoplar algumas funcionalidades ou a criação do objeto, como me indicou o Celestino, com um getInstance por exemplo.

De qualquer forma, fica a lição de que o teste feito quando usamos TDD é normalmente, pra não dizer: sempre, mais simples.

segunda-feira, 7 de janeiro de 2008

Devo testar os métodos que não alteram parâmetros nem retornam nada?

Isso é uma questão meio filosófica, pois, qual a necessidade de testar um método que não "faz nada" ?

Bem, se você ainda não imaginou a situação, lá vai um método da classe Manager:
0: public void reordenar(Object obj, boolean up)
1: {
2: getDao().reordenar(obj,up);
3: }
Com certeza o método reordenar da classe Dao faz muita coisa, e o teste dele será bem útil (sem nenhuma dúvida), mas o que faço com o método reordenar da classe Manager?

Começando pela premissa de que tudo pode e deve ser testado, então, devemos implementar um teste para este método.

Neste caso em particular, o método é muito simples, mas ele poderia não ser e ter a mesma característica, então, consideremos as várias possibilidades de quebra:

Primeiro imagine, que em qualquer parte do código pode haver um objeto nulo, como neste caso o getDao() poderia tranquilamente retornar null e disparar um NullPointerException na sequência.

Desta forma, é preferível fazer o teste sem nenhum "assert", porém, cobrindo cada linha do método:
0: public void testReordenar()
1: {
2: Object obj = new Object();
3: classeDao.expects(once()).method(
4: "reordenar").with(eq(obj),
5: eq(true));
6: classeManager.reordenar(obj, true);
7: }
Se você encontrar outra forma de fazer o teste e achar melhor, faça, mas não deixe de fazê-lo em hipótese alguma.