Testes Unitários na prática com Spring, TestNG, Mockito e Maven 2 : Parte 3

Na parte 1 desta séria de postes comentei sobre a integração de soluções para testes como o TestNG e o Mockito mostrando um domínio básico de um aplicação para efetuarmos testes. Na parte 2 mostrei na prática como criar um teste com Spring, testNG e rodar tudo através do Maven 2.

Neste post vou falar mais de alguns recursos do TestNG e mostrar como utilizar o Mockito e bem como as possibilidades e cenários de uso deste tipo de soluções para testes. Continuando o assunto com testNG vou falar de alguns recursos da solução utilizando anotações.



Anotações de @Before... e @After...

O TestNG tem algumas anotações muito úteis. Para setup de testes e grupos de testes, isso nos possibilita montar o ambiente necessário para execução dos testes. Também possibilita a limpeza do cenário dos testes entre uma execução e outra. Confira as anotações:
  • @BeforeClass
  • @AfterClass
  • @BeforeMethod
  • @AfterMethod
As anotações de Before e After significam que o método anotado vai rodar antes ou depois. Este antes ou depois pode ser da classe, método, grupo e suite. A cima estão listadas as principais anotações nesse sentido. Para a lista completa de anotações e outros recursos do TestNG confira na documentação.

Tempo de Execução de Métodos

Se a sua solução tem que lidar com requisitos de SLA, ou até mesmo requisitos não funcional como desempenho, você pode aferir isto através dos testes unitários. O TestNG através da anotações @Test tem o recurso timeOut. Assim você consegue especificar o tempo máximo para o método executar, assim você pode testar requisitos de SLA ou desempenho. Confira o código a baixo de exemplo:

package com.blogspost.diegopacheco.testng.spring.mockito.maven.core.service.test;

import static org.testng.Assert.assertNotNull;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
import org.testng.annotations.Test;

import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.service.VendaService;

/**
* Classe de testes para o Service de Vendas. Utilizando TestNG apénas.
* 
* @author Diego Pacheco
* @version 1.0
* @since 01/08/2009
*
*/
@Test(groups={"V.1.0"})
@ContextConfiguration(locations={"/spring-test-beans.xml"})
public class VendaServiceFuncTestNGTest extends AbstractTestNGSpringContextTests {

private VendaService vs;

@Test(timeOut=3)
public void testInjecaoSpringTimeOut(){
assertNotNull(vs,"O VendaService Não pdoe ser null");   
}

@Autowired
@Test(enabled=false)
public void setVs(VendaService vs) {
this.vs = vs;
}

}

Neste exemplo com Spring e TestNG o método testInjecaoSpringTimeOut tem que rodar em menos de 3s se este tempo passar o teste irá falhar. Você pode parametrizar este tempo para diversos métodos e diversos casos de teste.

O Nome do método ainda poderia ser algo que remetesse o requisito não funcional de desempenho ou requisito de SLA, sendo algo do tipo testInjecaoSpringTimeOutSLA002, isso ajuda na contextualização da aplicação e testes.

Testando código com dependências

Voltando ao nosso cenário do serviço de vendas. Este serviço depende do serviço de items que não existe ou não foi implementado ainda, mas você tem que criar os seus testes. Então temos algumas possibilidades. Vamos ver as nossas alternativas:
  • 1 - Não Criar o Teste Agora
  • 2 - Criar uma classe com implementação FAKE
  • 3 - Criar uma InnerClass com implementação FAKE
  • 4 - Utilizar o mockito e criar mocks nos pontos necessários
Bom a primeira opção vai depender muito de cada caso. No meu caso(aplicação fictícia) seria um risco deixar um ponto da aplicação tão importante sem testes. Dependendo da sua aplicação e dos seus requisitos e dos riscos associados ao testes é possível que certos pontos da aplicação não tenham testes.

A segunda opção é a mais usada tradicionalmente, ela é útil especialmente se você tiver muito código de teste a fazer ou se outros desenvolvedores também tenham que testar seu código e possam aproveitar seu mock, neste caso é uma boa idéia fazer isto. Do contrário não, por que é mais trabalho criar outra classe e adiciona-la ao testes, sem falar que não traz grandes vantagens.

A terceira opção é uma variação da segunda, porém é útil quando você quer realizar esta tarefa para apénas um teste, logo uma inner class é mais efetiva, menos trabalhosa e mais concisa. Confira o código a baixo para ver isto na prática.

package com.blogspost.diegopacheco.testng.spring.mockito.maven.core.service.test;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
import org.testng.Assert;
import org.testng.annotations.Test;

import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain.Comissao;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain.Item;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain.Produto;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain.Venda;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain.Vendedor;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.service.ItemService;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.service.VendaService;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.service.VendaServiceImpl;

/**
* Classe de testes para o Service de Vendas. Utilizando TestNG apénas.
* 
* @author Diego Pacheco
* @version 1.0
* @since 01/08/2009
*
*/
@Test(groups={"V.1.0"})
@ContextConfiguration(locations={"/spring-test-beans.xml"})
public class VendaServiceFuncParametersTestNGTest extends AbstractTestNGSpringContextTests {

private VendaService vs;

public void testInnerClassVender(){

Vendedor vendedor = new Vendedor();
vendedor.setId(102039L);
vendedor.setNome("Ricardo");

Produto p1 = new Produto();
p1.setId(1L);
p1.setNome("Mouse pad x");
p1.setDesc("Suporte para mouse");

Item i1 = new Item();
i1.setId(1L);
i1.setPreco(20D);
i1.setQuantidade(2);
i1.setProduto(p1);

List produtos = new ArrayList();
produtos.add(i1);

Venda v = new Venda();
v.setId(1001L);
v.setVendedor(vendedor);
v.setItems(produtos);

((VendaServiceImpl)vs).setItemService(getItemService());       
Comissao comissao = vs.vender(v);

Assert.assertNotNull(comissao,"O metodo vender do servico de vende deve retornar um objeto comissao. Comissao nullo");

}


private ItemService getItemService(){
return new ItemService(){
@Override
public void baixarEstoque(Item i) {
System.out.println("Estoque baixado com sucesso para o item: " + i);
}               
};
}

@Autowired
@Test(enabled=false)
public void setVs(VendaService vs) {
this.vs = vs;
}

}

Como podem ver para um caso simples como este já temos um esforço, em um cenário mais complexo seria pior ainda, mais a frente neste post vou mostrar como isso pode ser simplificado com o mockito, esta é a nossa quarta opção.

Utilizando um @DataProvider

Com este recurso você pode especificar provedores de dados para os testes, isso deixa a utilização dos testes mais limpa e mais focada. Sim, você deve estar pensando que poderia fazer a mesma coisa que esta anotação faz, sim esta certo, porem com esta anotação o código fica mais limpo e mais claro facilitando a leitura.

Você pode criar quantos provedores de dados quiser, para isso você precisa criar um método que retorne uma matriz de objetos, no método que for utilizar o recurso através da anotação @Test você tem que especificar o mesmo nome do provedor de dados. Na pratica, cada item será utilizado nos método na mesma ordem, então os tipos devem ser os mesmos, confira a utilização a baixo.

package com.blogspost.diegopacheco.testng.spring.mockito.maven.core.service.test;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
import org.testng.Assert;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain.Comissao;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain.Item;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain.Produto;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain.Venda;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain.Vendedor;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.service.ItemService;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.service.VendaService;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.service.VendaServiceImpl;

/**
* Classe de testes para o Service de Vendas. Utilizando TestNG apénas.
*
* @author Diego Pacheco
* @version 1.0
* @since 01/08/2009
*
*/
@Test(groups = { "V.1.0" })
@ContextConfiguration(locations = { "/spring-test-beans.xml" })
public class VendaServiceDataproviderTestNGTest extends    AbstractTestNGSpringContextTests {

@BeforeClass
public void setUp(){
((VendaServiceImpl) vs).setItemService(               
new ItemService() {
@Override
public void baixarEstoque(Item i) {
System.out.println("Estoque baixado com sucesso para o item: " + i);
}
}        
);
}

@Test(dataProvider="venda")
public void testVenderDataProvider(Venda v) {
Comissao comissao = vs.vender(v);
Assert.assertNotNull(comissao,"O metodo vender do servico de vende deve retornar um objeto comissao. Comissao nullo");
}

@DataProvider(name="venda")
public Object[][] createVenda() {
Vendedor vendedor = new Vendedor();
vendedor.setId(102039L);
vendedor.setNome("Ricardo");

Produto p1 = new Produto();
p1.setId(1L);
p1.setNome("Mouse pad x");
p1.setDesc("Suporte para mouse");

Item i1 = new Item();
i1.setId(1L);
i1.setPreco(20D);
i1.setQuantidade(2);
i1.setProduto(p1);

List produtos = new ArrayList();
produtos.add(i1);

Venda v = new Venda();
v.setId(1001L);
v.setVendedor(vendedor);
v.setItems(produtos);

return new Object[][] {
new Object[] { v }
};

}

private VendaService vs;

@Autowired
@Test(enabled = false)
public void setVs(VendaService vs) {
this.vs = vs;
}

}

Como podem reparar este código é parecido com o anterior, porém ele deixou o método de testes bem mais simples e objetivo. Além disso este provedor de dados chamado de venda poderia ser utilizado em mais de um método.

Utilizando o Mockito

Certo chegamos a quarta opção que mencionei antes. Dependendo do cenário esta é a melhor opção por que facilita mais ainda o código de testes, pois o mockito tem mecanismos sofisticados para lidar com mocks.

Vou mostrar um código de testes semelhante aos dois códigos anteriores só que utilizando o mockito, confira o código a baixo:

package com.blogspost.diegopacheco.testng.spring.mockito.maven.core.service.test;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain.Comissao;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain.Item;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain.Produto;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain.Venda;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain.Vendedor;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.service.ItemService;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.service.VendaService;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.service.VendaServiceImpl;

/**
* Classe de testes para o Service de Vendas. Utilizando TestNG e Mockito.
* 
* @author Diego Pacheco
* @version 1.0
* @since 01/08/2009
*
*/
@Test(groups={"V.1.0"})
@ContextConfiguration(locations={"/spring-test-beans.xml"})
public class VendaServiceMockitoTestNGTest extends AbstractTestNGSpringContextTests {

private VendaService vs;

@Test(dataProvider="vendaMock")   
public void testVendaServiceVender(Venda v, VendaService vendaService){

Comissao comissao = vendaService.vender(v);
Assert.assertNotNull(comissao,"O metodo vender do servico de vende deve retornar um objeto comissao. Comissao nullo");

}

@DataProvider(name="vendaMock")
public Object[][] createVendaMock(){

// Vendedor mockado
Vendedor vendedor = mock(Vendedor.class);
when(vendedor.getId()).thenReturn(102039L);
when(vendedor.getNome()).thenReturn("Ricardo");

// Produto mockado
Produto p1 = mock(Produto.class);
when(p1.getId()).thenReturn(1L);
when(p1.getNome()).thenReturn("Mouse pad x");
when(p1.getDesc()).thenReturn("Suporte para mouse");

// Item Mockato
Item i1 = mock(Item.class);
when(i1.getId()).thenReturn(1L);
when(i1.getPreco()).thenReturn(20D);
when(i1.getQuantidade()).thenReturn(2);
when(i1.getProduto()).thenReturn(p1);

// List normal, nao eh mockado
List produtos = new ArrayList();
produtos.add(i1);

// Venda Mockada
Venda v = mock(Venda.class);
when(v.getId()).thenReturn(1001L);
when(v.getVendedor()).thenReturn(vendedor);
when(v.getItems()).thenReturn(produtos);

ItemService is = mock(ItemService.class);
((VendaServiceImpl)vs).setItemService( is );

return new Object[][] {
new Object[] { v , vs }
};       
}

@Autowired
@Test(enabled=false)
public void setVs(VendaService vs) {
this.vs = vs;
}

}

Você se lembra quando criei a inner classe para o serviço de items ? Pois é com o mockito este tercho de código ficou muito mais simples. Isto foi realizado com apénas uma linha de código, além disso o mockito foi utilizado para mockar os parametros do método.

Normalmente não é necessário mockar os parametros dos métodos quando eles são pojos, agora quando o serviço que esta sendo testado depende de outros serviços ai sim precisamos de mocks. Realizei o mock nos pojos de entrada apénas para mostrar algumas funcionalidades do mockito.

A DSL do Mockito

O que mais me agrada no mockito é a sua DSL, no teste a cima utilizei alguns elementos desta DSL que vou explicar melhor agora, então vamos lá, na ordem de utilização.
  • mock
  • when
  • thenReturn
Quando usamos mock passamos por parametro uma class, o mockito vai criar esta classe adicionado os recursos de mock a este objeto. Com o when estamos especificando qual é o método a ser mockado, também é necessário informar os parametros desse método. Com isso é possível ter comportamento diferente para o mesmo método com parametros diferentes. Por fim utilizamos o thenReturn com ele especificamos o retorno do método.

Ainda é possível mockar exceptions, isso é grande recurso e também faz parte da DSL do Mockito, você faz isso com o doThrow e depois utiliza o when para especificar o método, confira o exemplo a baixo:

List mock = mock(List.class);
doThrow(new RuntimeException("Erro gerado via mockito.")).when(mock).clear();

try{
mock.clear();
}catch(Exception e){
System.out.println(e.getMessage());
}

O Mockito ainda tem muitas outras funcionalidades como mockar métodos void e lidar com ordem de chamadas de métodos, a sua grande vantagem sobre os outros frameworks de mocks é a simplicidade e praticidade da sua DSL.

Conclusão

Testar pode ser bem complicado quando quando temos facilidades em frameworks como Spring, TestNG e Mockito parte deste custo fica diluido. Para efetuar os testes nesta série de postes foi relativamente fácil por que o código produzido era testável. O Desenvolviemnto orientado a interfaces e Spring dixa o código mais testável por natureza, caso o código não fosse testável iria ser necessário aplicar refactoring no código e até mesmo um re-design dependendo do caso.

Em casos em que realizar um refactoring é complicado para utilizarmos testes aspectos são necessários, isto é ruim por que o custo para realizar a construção do teste fica muito elevada, mas ainda sim seria possível. Pensar em testes des do inicio do desenvolvimento além de uma excelente prática é uma forma de diminuir os riscos e melhorar a qualidade da aplicação.

Se você quiser pode obter os fontes completos desta aplicação que montei para a série de posts no meu repositório do Subversion neste URL.

Abraços e até a próxima.

Popular posts from this blog

Telemetry and Microservices part2

Installing and Running ntop 2 on Amazon Linux OS

Fun with Apache Kafka