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

No post anterior eu comecei a falar de testes com Spring, TestNG, Mockito e Maven. Neste post vou continuar com o assunto. Agora direto ao código e com exemplo práticos, usarei isto para discutir mais sobre o tema.

Vamos ao código então, vou mostrar primeiro o código do domínio e depois os serviços para por fim chegar no nosso objetivo os testes.



Produto

package com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain;

import java.io.Serializable;

/**
* Pojo que representa a entidade Produto.
* O produto pode ser qualquer mercadoria que eh vendida pelo estabelecimento.
* Você pode adicionar mapeamento ORM aqui com anotações por exemplo, isso foi
* abstraido por conta do foco do post.
*
* @author Diego Pacheco
* @version 1.0
* @since 28/07/2009
*
*/
public class Produto implements Serializable {

private static final long serialVersionUID = 1L;

private Long id;
private String nome;
private String desc;

public Produto(){ }

public Produto(Long id, String nome, String desc) {
super();
this.id = id;
this.nome = nome;
this.desc = desc;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getNome() {
return nome;
}

public void setNome(String nome) {
this.nome = nome;
}

public String getDesc() {
return desc;
}

public void setDesc(String desc) {
this.desc = desc;
}

public String toString(){
return "id: " + id + ", nome: " + nome + ", descricao: " + desc;
}

}

Item

package com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain;

import java.io.Serializable;

/**
* Pojo que representa a entidade Item. Este eh soh um exemplo de modelagem, você poderia modelar de outras formas. Nesta caso
* o valor do produto e o preco ficam no item, assim podemos lidar com precos promocionais e vendas em lote.
* 
* Você pode adicionar mapeamento ORM aqui com anotações por exemplo, isso foi
* abstraido por conta do foco do post.
*
* @author Diego Pacheco
* @version 1.0
* @since 28/07/2009
*
*/
public class Item implements Serializable {

private static final long serialVersionUID = 1L;

private Long id;
private Produto produto;
private Integer quantidade;
private Double preco;

public Item() {
super();
}

public Item(Long id,Produto produto, Integer quantidade, Double preco) {
super();
this.id = id;
this.produto = produto;
this.quantidade = quantidade;
this.preco = preco;
}

public Produto getProduto() {
return produto;
}

public void setProduto(Produto produto) {
this.produto = produto;
}

public Integer getQuantidade() {
return quantidade;
}

public void setQuantidade(Integer quantidade) {
this.quantidade = quantidade;
}

public Double getPreco() {
return preco;
}

public void setPreco(Double preco) {
this.preco = preco;
}   

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String toString(){
return "Produto: {" + produto + "}, quantidade: " + quantidade + ", preco: " + preco;
}   

}

Vendedor

package com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain;

import java.io.Serializable;

/**
* Pojo que representa a entidade Vendedor.
* O Vendedor eh quem vende os produtos.
* Você pode adicionar mapeamento ORM aqui com anotações por exemplo, isso foi
* abstraido por conta do foco do post.
*
* @author Diego Pacheco
* @version 1.0
* @since 28/07/2009
*
*/
public class Vendedor implements Serializable {

private static final long serialVersionUID = 1L;

private Long id;
private String nome;

public Vendedor() {
super();
}

public Vendedor(Long id, String nome) {
super();
this.id = id;
this.nome = nome;
}

public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}

public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}

public String toString(){
return "id: " + id + ", nome: " + nome;
}

}

Comissao

package com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain;

/**
* Pojo que representa a entidade Comissao. A sua resposabilidade eh de
* representar a comissao de uma venda.
*
* @author Diego Pacheco
* @version 1.0
* @since 28/07/2009
*
*/
public class Comissao {

private Long id;
private Venda venda;
private Double valor;

public Comissao() {
super();
}

public Comissao(Long id, Venda venda, Double valor) {
super();
this.id = id;
this.venda = venda;
this.valor = valor;
}

public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}

public Venda getVenda() {
return venda;
}
public void setVenda(Venda venda) {
this.venda = venda;
}

public Double getValor() {
return valor;
}
public void setValor(Double valor) {
this.valor = valor;
}



}

Venda

package com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain;

import java.io.Serializable;
import java.util.List;

/**
* Pojo que representa a entidade Venda. Este eh soh um exemplo de modelagem, você poderia modelar de outras formas. Nesta caso
* o pojo Venda tem relacao com o Vendedor e uma lista de Item.
* 
* Você pode adicionar mapeamento ORM aqui com anotações por exemplo, isso foi
* abstraido por conta do foco do post.
*
* @author Diego Pacheco
* @version 1.0
* @since 28/07/2009
*
*/
public class Venda implements Serializable {

private static final long serialVersionUID = 1L;

private Long id;
private List items;
private Vendedor vendedor;

public Venda() {
super();
}

public Venda(Long id, List items, Vendedor vendedor) {
super();
this.id = id;
this.items = items;
this.vendedor = vendedor;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public List getItems() {
return items;
}

public void setItems(List items) {
this.items = items;
}

public Vendedor getVendedor() {
return vendedor;
}

public void setVendedor(Vendedor vendedor) {
this.vendedor = vendedor;
}

public String toSring(){
return "id: " + id + ", Items: {" + items + "}, vendedor: {" + vendedor + "}";
}

}

Se você caio aqui de para-quedas agora pode conferir o post anterior que tem uma explicação mais detalhada sobre este domínio. Vamos ao código dos serviços, estes sim irei explicar mais detalhadamente neste post.

ItemService

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

import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain.Item;

/**
* Contrato do service de Item. Neste caso eh resposável por dar baixa ao estoque.
*
* @author Diego Pacheco
* @version 1.0
* @since 28/07/2009
*
*/
public interface ItemService {   

public void baixarEstoque(Item i);

}

Este serviço opera sobre o Item, neste exemplo ele define o contrato para operação de dar baixa no estoque do item. Caso o item em questão ao ser baixado não exista mais no estoque o serviço deve lançar uma exception.

Neste exemplo vamos supor que não existe implementação para o serviço de items, porem você tem que implementar o serviço de Vendas e além de implementar vou mostrar como testar o mesmo.

Então vamos ver o código do serviço de Vendas primeiro começando pelo contrato(interface) e depois indo para a implementação padrão.

VendaService

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

import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain.Comissao;
import com.blogspost.diegopacheco.testng.spring.mockito.maven.core.domain.Venda;

/**
* Contrato do service de Vendas. Que deve ser capaz de realizar uma venda e bem como estornar a mesma.
* 
* @author Diego Pacheco
* @version 1.0
* @since 28/07/2009
*
*/
public interface VendaService {

public Comissao vender(Venda v);
public void estornar(Venda v);

}

Como podem ver quem implementar o contrato do serviço de vendas deverá implementar estes dois métodos, sendo que o método estornar vai ser implementado somente na próxima versão, mas para está versão você deverá implementar o método vender.

Então vamos a implementação deste serviço com Spring e anotações, sempre usando e abusando na injeção de dependências.

VendaServiceImpl

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

import org.springframework.stereotype.Component;

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.Venda;

/**
* Implementacao padrao do service de vendas. Este service eh basico, ele acessa outros services
* para dar baixa do estoque e realizar comicionamento por exemplo.
* 
* @author Diego Pacheco
* @version 1.0
* @since 28/07/2009
*
*/
@Component
public class VendaServiceImpl implements VendaService{

private ItemService itemService;

@Override
public Comissao vender(Venda v) {       

// Criticas básicas do service.

if (v==null)
throw new IllegalArgumentException("A venda nao pode ser nula. ");
if(v.getVendedor()==null)
throw new IllegalArgumentException("A venda deve ter um vendedor. Vendedor nao pode ser nulo. ");
if(v.getItems()==null || v.getItems().size()==0)
throw new IllegalArgumentException("A venda deve possuir produtos. Produtos nulos. ");

// RN002 - Calcular total da venda para comicionamento.

Double total = 0.0;
for(Item i: v.getItems()){
total += (i.getPreco() * i.getQuantidade());

// RN001 - Dar baixa do estoque se possivel, e nao abortar venda.
itemService.baixarEstoque(i);

}
Comissao c = new Comissao();
c.setId(v.getId());
c.setValor(total/2);
c.setVenda(v);

return c;
}

@Override
public void estornar(Venda v) {
throw new UnsupportedOperationException("O metodo estornar. Vai ser implementado na versao 2.0 ");
}

public ItemService getItemService() {
return itemService;
}

public void setItemService(ItemService itemService) {
this.itemService = itemService;
}   

}

Neste serviço estou implementado as duas regras de negócio fictícias desta aplicação. Como pode reparar estou utilizando a interface do serviço de vendas, mesmo que existisse uma implementação o certo é usar sempre a interface. Perceba que no meio da regra de negócio do meu serviço de vendas eu dependo de uma chamada do serviço de item que por sinal não existe ainda.

Ok. Agora vamos o setup neceário no maven para utilziar Spring, TestNG e Mockito. Vou mostrar as dependêncdias no pom.xml e depois a configuração do plugin do maven para integração com TestNG.

Dependências do pom.xml




org.testng
testng
5.8
jdk15
test


org.springframework
spring-test
2.5.6
test


org.springframework
spring-context
2.5.6


org.springframework
spring-aop
2.5.6
test


org.springframework
spring-aspects
2.5.6
test
       

org.springframework
spring-beans
2.5.6
test


org.aspectj
aspectjrt
1.5.4
test


org.aspectj
aspectjweaver
1.5.4
test



Como ja disse no post anterior esta aplicação é reduzida, além disso o foco do post são os testes e como configurar e executar os mesmos em um projeto Java com Spring. Mas para isso eu tive que fornecer o mínimo de imformações sobre a aplicação de exemplo para que poçamos chegar nos testes.

Certo. Vamos a configuração do plugin do Maven...




org.apache.maven.plugins
maven-surefire-plugin
2.4


${basedir}/src/test/resources/testng.xml





Este é o plugin de ralorios do maven, estou integrando ele as configurações do testNG. Este arquivo chamado de testing.xml é onde ficam as configurações dos sutites de testes.

Confira o conteúdo do arquivo a baixo:












O que são os Suites de testes do TestNG ?

São as configurações para rodar testes em grupos. Isto é muito útil pois você pode etar desenvolvendo mais de uma versão da sua aplicação ao mesmo tempo. Ja vi casos em que 3 versões são desenvolvidas ao mesmo tempo. Logo é muito útil para o desenvolvedor poder testar apénas o que é da sua versão.

Com o arquivo testing.xml eu configurei um test suite para a versão 1.0 da aplicação. Você pode ter vários suites e organiza-los de diversas formas por exemplo: por versão, requisitos, funcionalidades, interação/sprint, riscos, etc...

Ainda é possível colocar o mesmo teste em mais de um suite.

O Maven vai executar os testes unitários do testNG dentro do seu cliclo de vida principal, ou seja, quando você rodar o comando: $ mvn clean install ele ja vai rodar os testes. Se por acaso algúm dos testes venham a falahar o build é abortado e o deploy ou package não é feito.

Este tipo de comportamento é excelente e muito utilizando em soluções de integração continua por que se os seus testes tem problemas, sua aplicação não esta pronta para produção, com esse mecanismo o maven ajuda que você não libere uma aplicação com problemas nos testes.

Enfim agora podemos ir para os testes... Piomeiro vou mostrar como criar uma classe de testes unitários usando Spring e TestNG, no próximo post vou entrar mais afundo nos testes e no Mockito.

Como esta aplicação é baseada em Spring vamos ao arquivo de configuração do contexto do Spring, neste caso a aplicação é orientada a anotações então o arquivo é simples mesmo.

spring-test-beans.xml











Este arquivo esta informando para o Spring que ele deve fazer uma varredura no pacote com.blogspost.diegopacheco.testng e sub-pacotes procurando por componentes do Spring. Isso é uma mão na roda e aumenta consideravelmente a produtividade de sua equipe.

Agora vamos a nossa primeira implementação de testes unitários com o TestNG e o Spring.

VendaServiceSimplesTestNGTest

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.domain.Venda;
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 30/06/2009
*
*/
@Test(groups={"V.1.0"})
@ContextConfiguration(locations={"/spring-test-beans.xml"})
public class VendaServiceSimplesTestNGTest extends AbstractTestNGSpringContextTests {

private VendaService vs;

public void testInjecaoSpring(){
assertNotNull(vs,"O VendaService Não pdoe ser null");       
}

@Test(expectedExceptions=IllegalArgumentException.class)
public void testParametroVendaNull(){
vs.vender(null);       
}

@Test(expectedExceptions=IllegalArgumentException.class)
public void testParametroVendedorNull(){
Venda v = new Venda();
vs.vender(v);       
}

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

}

Tenho muito o que explicar sobre esta classe... Então vamos por partes. Primeiro sobre as anotações que existem no topo da classe.
  • @Test(groups={"V.1.0"}): Indica que é um teste do TestNG referente a primeira versão da aplicação. Poderia ser outro grupo qualquer conforme dito antes.
  • @ContextConfiguration(locations={"/spring-test-beans.xml"}): Aqui estou dizendo para o Spring ser criado com este arquivo de configuração.
Perceba que a nossa classe extende outra classe que é do Spring, AbstractTestNGSpringContextTests é reposável por integrar o contexto do Spring para testes com o TestNG.

Depois vocês podem ver que existe a propriedade VendaService e um método setter com as seguintes anotações:
  • @Autowired: Indica que o Spring deve injetar um serviço de vendas, neste caso será VendaServiceImpl.
  • @Test(enabled=false): Para indicar que este método não deve ser testado.
Sobre a anotação @Test, se você usa testNG sozinho, cada método que você quer testar deve ter esta anotação por exemplo, neste caso esta anotação vou utilizanda apénas no método que não quero testar que é o método setter chamado setVs.

Como estamos extendendo AbstractTestNGSpringContextTests todos os método plublicos da classe serão testados.

Agora vamos falar um pouco mais sobre os 3 métodos de testes, que são eles:
  • testInjecaoSpring
  • testParametroVendaNull
  • testParametroVendedorNull
O primeiro método testInjecaoSpring testa se o Spring inetou uma instáncia do Serviço de Datas, isto é mais para ajudar na prevenção de pequenos erros do que no teste da aplicação mesmo. Neste método ainda você deve ter reparado o sequinte código:

assertNotNull(vs,"O VendaService Não pdoe ser null");       

Vamos por partes. O método assertNotNull vem do import estático feito no inicio da classe para:
import static org.testng.Assert.assertNotNull. Ele testa se 'vs' é diferente de null, caso 'vs' seja igual a null uma excpetion sera levantada e o teste irá falhar com a seguinte mensagem "O VendaService Não pdoe ser null".

Esta é a maneira adequade de se tetsar, usando asserções que são verificaçoes que dizem se o método que implementamos está retornando ou está realizando o comportamento que deveria fazer.

Neste momento o leitor pode achar que esto entrando em contradição, pois eu disse que não era necessário a anotação @Test, porem os dois outros métodos de testes tem ela, isso tem um mótivo.

No método testParametroVendaNull estou chamando o serviço de vendas passando null, se reparar no inicio do código deste serviço vai ver que existem alguma criticas e uma delas é se o objeto vendas é != null. Neste caso não é, logo o correto é que o serviço levante uma exception do tipo IllegalArgumentException, se ele não levantar esta exception é que esta errado, pois passamos um argumento errado e ele aceitou, ele levantando uma excpetion o teste esta correto pois é isto que esperamos.

O mesmo ocorre para o terceiro método o testParametroVendedorNull só que aqui testamos o cenário do vendedor ser nullo. Neste caso existe o objeto venda mas o objeto vendedor não. Logo também é esperada uma exception.

Outro ponto importante é a nomenclatura dos méotodos, quanto mais significativa melhor. Podem perceber que só pelo nome dos métodos você ja conseguem identificar o que eles fazem.

Se você rodar este código usando com $mvn clean install vai obter um resultado semelhante a este a baixo:

[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Building testng-spring-mockito-maven
[INFO]
[INFO] Id: com.blogspot.diegopacheco.sandbox.java:testng-spring-mockito-maven:jar:1.0
[INFO] task-segment: [clean, install]
[INFO] ------------------------------------------------------------------------
[INFO] [clean:clean]
[INFO] Deleting directory D:\Diego\Java\bin\workspaces-eclipse\eclipse_workspace\testng-spring-mockito-maven\target
[INFO] [resources:resources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:compile]
[INFO] Compiling 8 source files to D:\Diego\Java\bin\workspaces-eclipse\eclipse_workspace\testng-spring-mockito-maven\target\classes
[INFO] [resources:testResources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:testCompile]
[INFO] Compiling 1 source file to D:\Diego\Java\bin\workspaces-eclipse\eclipse_workspace\testng-spring-mockito-maven\target\test-classes
[INFO] [surefire:test]
[INFO] Surefire report directory: D:\Diego\Java\bin\workspaces-eclipse\eclipse_workspace\testng-spring-mockito-maven\target\surefire-reports

-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running TestSuite
31/07/2009 00:14:25 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [spring-test-beans.xml]
31/07/2009 00:14:26 org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.GenericApplicationContext@8916a2: display name [org.springframework.context.support.GenericApplicationContext@8916a2]; startup date [Fri Jul 31 00:14:26 BRT 2009]; root of context hierarchy
31/07/2009 00:14:26 org.springframework.context.support.AbstractApplicationContext obtainFreshBeanFactory
INFO: Bean factory for application context [org.springframework.context.support.GenericApplicationContext@8916a2]: org.springframework.beans.factory.support.DefaultListableBeanFactory@39443f
31/07/2009 00:14:26 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@39443f: defining beans [org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.config.internalBeanConfigurerAspect,vendaServiceImpl,org.springframework.aop.config.internalAutoProxyCreator]; root of factory hierarchy
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.323 sec

Results :

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0

31/07/2009 00:14:26 org.springframework.context.support.AbstractApplicationContext doClose
INFO: Closing org.springframework.context.support.GenericApplicationContext@8916a2: display name [org.springframework.context.support.GenericApplicationContext@8916a2]; startup date [Fri Jul 31 00:14:26 BRT 2009]; root of context hierarchy
31/07/2009 00:14:26 org.springframework.beans.factory.support.DefaultSingletonBeanRegistry destroySingletons
INFO: Destroying singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@39443f: defining beans [org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.config.internalBeanConfigurerAspect,vendaServiceImpl,org.springframework.aop.config.internalAutoProxyCreator]; root of factory hierarchy
[INFO] [jar:jar]
[INFO] Building jar: D:\Diego\Java\bin\workspaces-eclipse\eclipse_workspace\testng-spring-mockito-maven\target\testng-spring-mockito-maven-1.0.jar
[INFO] [source:jar]
[INFO] Building jar: D:\Diego\Java\bin\workspaces-eclipse\eclipse_workspace\testng-spring-mockito-maven\target\testng-spring-mockito-maven-1.0-sources.jar
[INFO] [install:install]
[INFO] Installing D:\Diego\Java\bin\workspaces-eclipse\eclipse_workspace\testng-spring-mockito-maven\target\testng-spring-mockito-maven-1.0.jar to C:\Users\Diego Pacheco\.m2\repository\com\blogspot\diegopacheco\sandbox\java\testng-spring-mockito-maven\1.0\testng-spring-mockito-maven-1.0.jar
[INFO] Installing D:\Diego\Java\bin\workspaces-eclipse\eclipse_workspace\testng-spring-mockito-maven\target\testng-spring-mockito-maven-1.0-sources.jar to C:\Users\Diego Pacheco\.m2\repository\com\blogspot\diegopacheco\sandbox\java\testng-spring-mockito-maven\1.0\testng-spring-mockito-maven-1.0-sources.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 6 seconds
[INFO] Finished at: Fri Jul 31 00:14:27 BRT 2009
[INFO] Final Memory: 5M/21M
[INFO] ------------------------------------------------------------------------

Agora temos o nosso primeiro teste usando Spring, TestNG, conceitos e práticas de testes unitários e maven. No próximo post vou mostrar mais testes com o TestNG e vou apresentar o Mockito, este post seguinte será focado somente nos testes então não percam.

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