Na parte 01 desta séria de artigos 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 02, mostrei, na prática, como criar um teste com Spring, testNG e rodar tudo através do Maven 2.
Neste artigo vou falar mais de alguns recursos do TestNG e mostrar como utilizar o Mockito
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. Acima 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 funcionais 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 abaixo 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 3 segundos. 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 itens 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:
- Não Criar o Teste Agora
- Criar uma classe com implementação FAKE
- Criar uma InnerClass com implementação FAKE
- 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 adicioná-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 apenas um teste, logo
uma inner class é mais efetiva, menos trabalhosa e mais concisa.
Confira o código abaixo 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<item> produtos = new ArrayList<item>();
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 à frente neste artigo 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. Você
deve estar pensando que poderia fazer a mesma coisa que esta anotação
faz. Sim, está certo. Porém, 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étodos na
mesma ordem, então os tipos devem ser os mesmos, confira a utilização abaixo.
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<item> produtos = new ArrayList<item>();
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 à quarta opção, que mencionei antes. Dependendo do cenário esta
é a melhor opção, porque 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 abaixo:
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<item> produtos = new ArrayList<item>();
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 de quando criei a
inner classe para o serviço de itens? Pois é, com o mockito este trecho
de código ficou muito mais simples. Isto foi realizado com apenas 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
parâmetros dos métodos quando eles são pojos. Agora, quando o serviço
que está sendo testado depende de outros serviços, aí, 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 acima 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 parâmetro 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 abaixo:
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 temos facilidades em frameworks,
como Spring, TestNG e Mockito, parte deste custo fica diluido. Efetuar os testes nesta série de artigos foi relativamente fácil porque
o código produzido era testável. O Desenvolvimento orientado a
interfaces e Spring deixa o código mais testável por natureza, caso o
código não fosse testável seria 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 porque o custo para realizar a
construção do teste fica muito elevado, mas ainda sim seria possível.
Pensar em testes desde o início 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.