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.