Praticando Test Driven Development – TDD

Olá,

Depois de algum tempo sem postar nada, eis que vendo um probleminha no GUJ resolvi praticar um pouquinho de TDD e assim escrever um post sobre essa excelente metodologia.
Já li muito sobre o assunto e tenho até uma boa base teórica. O TDD é bem simples e fácil de entender, mas ainda não pratiquei muito codificando, confesso que por um pouco de preguiça, e com este probleminha foi uma boa oportunidade de praticar.

O problema que eu escolhi foi o cálculo do Número de Euller(e) usando a Série de Taylor dada por:
e = 1 + 1/1! + 1/2! + 1/3! + 1/4! + …

Onde teremos um valor de entrada n, que quanto maior ele for, mais aproximado ele será do número de Euller.

Primeiramente, para facilitar o cálculo da série, vamos implementar a função fatorial(n) que como sabe-se é calculada da seguinte forma:

fatorial(n): = 1, se n = 0;
                     = n * fatorial(n-1), se n > 0;
                     indefinida, c.c


Inicialmente criamos nosso Test Case utilizando o framework JUnit. Criamos então a classe de teste FatorialTest, onde inicialmente implementaremos pensando no caso base do fatorial.
Assim criamos o seguinte teste:

@Test
public void deveriaRetornarUm() {
	assertEquals(new Integer(1), Operacao.fatorial(1));
}

E rodando a classe teremos a famosa barra vermelha, já que não temos um método fatorial criado/implementado:

Para nos livrarmos deste erro, vamos criar o corpo do nosso método estático fatorial, que inicialmente retornará null. E em seguida vamos rodar o nosso Case Test novamente. Eis que temos um novo erro:

Normal, pois estamos retornando um valor nulo, onde esperamos um Integer. Assim, alteraremos nosso método para:

public static Integer fatorial(Integer i) {
		return 1;
}

Assim, teremos nossa desejada primeira barrinha verde.
Mas espere que ainda não acabou. Vamos escrever um novo case test para retornar o valor do fatorial de dois:

@Test
public void deveriaRetornarFatorialDeDois(){
	assertEquals(new Integer(2), Operacao.fatorial(2));
}

Ao rodar o teste, teremos uma nova barrinha vermelha, já que esperamos 2, e o método retornou 1.
Assim, refatoramos nosso método para que ele possa retornar o valor correto:

public static Integer fatorial(Integer i) {
	if(i.equals(1)){
		return 1;
	}
	return 2;
}

Agora temos nossa barrinha verde novamente, pois o método retorna valores corretos para os dois valores.

O próximo passo é criar o teste para cálculo do fatorial de 3, e vemos que ao rodar a classe de teste teremos uma nova barra vermelha pois o nosso método ainda não implementa corretamente o fatorial:

@Test
public void deveriaRetornarFatorialDeTres(){
	assertEquals(new Integer(6), Operacao.fatorial(3));
}

Poderiamos refatorar nosso método e colocar mais um if fazendo com que o teste passe, mas irei pular alguns passos neste post e implementar o algoritmo desta função.
É importante implementar e retornar cada valor para que possamos ficar seguro com o que o método deve fazer. Omiti esses outros passos para não me prolongar. Mas creio que a ideia tenha sido assimilada.
Assim teremos o nosso novo método fatorial, da seguinte forma:

public static Integer fatorial(Integer n) {
	if(n == 0){
		return new Integer(1);
	}
	return n*fatorial(n-1);
}

E então nossa classe de teste ficará assim:

public class FatorialTest {

	@Test
	public void deveriaRetornarUm() {
		assertEquals(new Integer(1), Operacao.fatorial(0));
		assertEquals(new Integer(1), Operacao.fatorial(1));
	}

	@Test
	public void deveriaRetornarFatorialDeDois(){
		assertEquals(new Integer(2), Operacao.fatorial(2));
	}

	@Test
	public void deveriaRetornarFatorialDeTres(){
		assertEquals(new Integer(6), Operacao.fatorial(3));
	}

	@Test
	public void deveriaRetornarFatorialDeCinco(){
		assertEquals(new Integer(120), Operacao.fatorial(5));
	}

	@Test
	public void deveriaRetornarFatorialDeDez(){
		assertEquals(new Integer(3628800), Operacao.fatorial(10));
	}
}

]
E aí, estamos esquecendo de alguma coisa? Lembre-se que nossos testes devem ter a maior Cobertura de Testes possivel e está faltando cobrir um caso. Alguém ai conseguiu identificar?
E se testarmos com um número negativo, o que acontece?

@Test
public void deveriaRetornarNulo() {
	assertNull(Operacao.fatorial(-10));
}

Teremos uma barra vermelha e nosso teste quebra. Assim refatoramos nosso método novamente e teremos nossa última versão do fatorial:

public static Integer fatorial(Integer n) {
	if(n < 0){
		return null;
	}
	else{
		if(n == 0 || n == 1)
			return 1;
	}
	return n*fatorial(n-1);
}

E finalmente ao rodar nossos testes recebemos nossa green bar e terminamos. Espere, vamos adicionar só o seguinte teste:

@Test
public void naoDeveriaRetornarFatorialDeSeis(){
	assertFalse((new Integer(600)).equals(Operacao.fatorial(6)));
}

O que ele faz? Estamos simulando que o teste será errado, e como queriamos o método retorna falso corretamente.
Assim finalizamos o nosso método fatorial e se você testar, verá que ele está implementado corretamente. 😉

Mas ainda temos mais um probleminha. E o fatorial(18)? Como sabe-se os valores do tipo Integer são limitados até o valor: 2^31 – 1 = 2147483647.
Para valores acima disto nosso método terá problemas. Então vamos refatorar nossos testes e nosso método trocando o tipo Integer pelo tipo BigDecimal e alterando também a forma como calculamos, já que para utilizar BigDecimal temos que usar seus métodos para realizar os cálculos.
Então, teremos uma versão mais poderosa do nosso método fatorial:

public static BigDecimal fatorial(Integer n) {
	if(n < 0){
		return null;
	}
	else{
		if(n == 0 || n == 1)
			return BigDecimal.ONE;
	}
	return fatorial(n-1).multiply(new BigDecimal(n));
}

E enfim terminamos nossa implementação. E vamos para o nosso método que calculará a Série de Taylor.

Da mesma forma que o método fatorial eu fui criando e evoluindo o método calculaSerieDeTaylor() aos poucos, utilizando Passinhos de bebê(ou Baby Steps), que consiste em dar um passo seguro de cada vez na evolução da implementação do seu método.
Eu poderia repetir e postar como fiz passo-a-passo, mas acho que a idéia já deu pra pegar na implementação anterior, então para economizar linhas repetitivas, ao final terei o seguinte código do método e dos case tests:

public static BigDecimal calculaSerieDeTaylor(int n){
	BigDecimal e = BigDecimal.ONE;

	if(n <= 0){
		return BigDecimal.ZERO;
	}else{
		for(int i = 1; i < n; i++){
			//Utilizei 20 casas decimais, mas isso depende da necessidade de querer mais ou menos precisão.
			e = e.add(BigDecimal.ONE.divide(fatorial(i), 20, RoundingMode.UP));
		}
	}
	return e;
}
@Test
public void deveriaRetornarZeroParaValorNegativo() {
	assertEquals(BigDecimal.ZERO, Operacao.calculaSerieDeTaylor(-20));
}

@Test
public void deveriaRetornarSerieDeZero(){
	assertEquals(BigDecimal.ZERO, Operacao.calculaSerieDeTaylor(0));
}

@Test
public void deveriaRetornarSerieDeUm() {
	assertEquals(BigDecimal.ONE, Operacao.calculaSerieDeTaylor(1));
}

@Test
public void deveriaRetornarSerieDeDois() {
	assertEquals(new BigDecimal("2").setScale(20), Operacao.calculaSerieDeTaylor(2));
}

@Test
public void deveriaRetornarSerieDeTres() {
	assertEquals(new BigDecimal("2.5").setScale(20), Operacao.calculaSerieDeTaylor(3));
}

@Test
public void deveriaRetornarSerieDe4() {
	assertEquals(new BigDecimal("2.66666666666666666667"), Operacao.calculaSerieDeTaylor(4));
}

Você pode rodar mais testes, e vai ver que quanto mais você aumenta o n, mais ele converge para o número de Euller, que é o que nós queremos.

Outra coisa super importante que é possível perceber quando escrevemos nossos testes e quando estes estão bem cobertos, é que quando temos alguma mudança eles nos protegem de acontecer quebras no nosso código.
Por exemplo, o nosso método calculaSerieDeTaylor pode ficar muito lento com valores de n bem altos. Isso se deve do fato do método fatorial está sendo implementado de maneira recursiva.
Todos nós estudantes de algum curso de Computação, sabemos que há uma maneira bem mais eficiente de implementar esta função. É a maneira iterativa, utilizando um array e percorrendo seus elementos e calculando sobre ele.

Assim, para valores de n grandes, temos um ganho considerável na perfomance do cálculo de nossa série. Podemos alterar nosso método fatorial e executar nossos testes normalmente e verificar que ele ainda funciona
da mesma maneira, só que mais eficiente. Com os testes nós temos a garantia que o comportamento do método não foi alterado que era o que nós esperávamos.
Nosso novo método pode ser reescrito desta forma:

public static BigDecimal fatorial(Integer n) {
	if(n > 1){
		BigDecimal[] vet = new BigDecimal[n+1];
		vet[0] = vet[1] = new BigDecimal("1");
		for(int i = 2; i &lt;= n; i++){
			vet[i] = vet[i-1].multiply(new BigDecimal(i));
		}
		return vet[n];
	}
	return BigDecimal.ONE;
}

Após fazer uma pequena alteração nos asserts da classe de testes para comparar com BigDecimal ao invés de Integer, rodamos os testes e temos nossa green bar novamente, o que nos deixa felizes e contentes, pois sabemos que tudo funciona da mesma forma.
Posteriormente caso necessite de mais alterações, como por exemplo o calculo da série para e^x, poucas alterações serão feitas e muito código pode ser reaproveitado. Percebeu como você ganha agilidade?
_

Conclusão: Utilizando o TDD podemos evoluir nosso código de maneira segura e eficiente. Outra dica que eu dou vem de um artigo que eu li e achei muito interessante:
Quando for implementar seu teste “Diga o que deseja ao invés de perguntar”. Assim fica mais claro saber o comportamento que seu método terá e você passará a entendê-lo melhor.

Espero ter sido claro e conciso. Quem quiser contribuir com mais dicas e melhorias no post fique à vontade. Toda critica/sugestão é super bem-vinda.

Abraços 😉

_

Códigos no github:
https://github.com/henriqueluz/praticando-tdd

Anúncios

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s