domingo, 20 de abril de 2008

Testes de Unidade para camada de negócios usando BDUnit

Introdução

Com a aproximação das áreas de negócio e conceitos atuais como SOA e BPM, o usuário está habilitando-se a visualizar de forma mais clara o funcionamento interno do software, e com isso garantindo que o mesmo faça o que realmente se propoe a fazer. Tendo em vista que os usuários endendem o "workflow" dos seus processos e como eles estão organizados (através de orquestração) dentro do software é imprescindível que todas as unidades que compõem os processos funcionem de acordo com o esperado.
Paralelamente a isso, o crescimento e consequente popularização das metodologias de desenvolvimento ágil, tem levado os desenvolvedores a escrever Testes de Unidade em seus projetos. Tais testes, além de servirem para validar a corrente qualidade das unidades do software, servem também como uma ótima documentação (definida programaticamente) para quem vai utilizar as unidades em produção.
Quando se pensa em Testes de Unidade é necessário ter em mente qual o nível ou camada do software eles devem atingir, isso implica diretamente na escolha das tecnologias (frameworks) e técnicas a serem utilizadas. Todas as camadas de um software podem ser testadas, tais como: camada de persistência, camada de negócios, camada de aplicação, ou até mesmo a camada de apresentação (de interface com o usuário). Dentre todas as camadas a serem testadas, destaca-se a camada de negócios. Esta, como o próprio nome sugere, diz respeito diretamente ao domínio do negócio do cliente. Os testes desta camada devem testar as ditas Regras do Negócio, elas compõem o ponto crítico do sistema, pois são estas regras que determinam o funcionamento dos processos.

Porque começar a testar o negócio pela camada de negócios e não pela camada de aplicação?

Para explicar isso imagine o seguinte dialogo entre dois desenvolvedores Java:
Des1: - Meu teste deve começar da onde (qual camada)?
Des2: - Depende, qual o foco do seu teste?
Des1: - Ah, o meu foco agora é testar a segurança da aplicação, bem como outras questões relacionadas à infraestrutura.
Des2: - Okay, então o seu teste deve começar pela camada de aplicação (testar as Actions do Struts, Logicas do VRaptor, etc). Quando seu teste diz respeito à Infraestrutura da aplicação então ela deve iniciar no mínimo pela camada de aplicação.
Des1: - Entendi, mas como vou testar a minha camada de aplicação se nela eu utilizo recursos cedidos pelo servidor de aplicação/servlet container, tipo uma Session, etc.
Des2: - Simples, utilize Mock Objects, com eles pode-se criar "falsas implementações" das classes/interfaces que são utilizadas na camada de aplicação e que não são controladas por você.
Des1: Até aí tudo bem, mas agora imagine que meu foco é testar as regras de negócio do meu cliente, por que eu não deveria continuar iniciando meus testes pela camada de aplicação?
Des2: Você não quer testar as regras de negócio do seu cliente?
Des1: Sim, por que?
Des2: Então qual o motivo de você querer poluir o código de testes com questões da infraestrutura (cheio de mock objects no meio atrapalhando a visibilidade da real intenção do teste)
Des1: Faz sentido, assim quem for ler o código de testes vai entender como o negócio deve ser usado, independente de infraestrutura.
Des2: Exatamente! :D

Testando aplicações baseadas em banco de dados (normalmente sistemas)

Agora que já sabe-se a necessidade dos testes e qual camada eles devem atingir, algumas regras precisam ser seguidas, são elas:
- um método de teste não deve depender do estado da base de dados para funcionar;
- um método de teste não deve depender de outro para funcionar;
- quando um caso de teste tiver uma complexidade consideravelmente mais alta, este deve ser dividido em pequenos métodos de teste, por exemplo: testAdicionarClienteSemEndereco() ou testAdicionarClienteCpfInvalido(), etc;
- um método de teste deve ser capaz de preparar a base de dados para que ele funcione independentemente.

Cumprindo as regras anteriores usando os frameworks JUnit4 e DBUnit 2.2

JUnit é um dos mais conhecidos frameworks para execução de testes em Java, a versão 4 permite que o programador utilize anotações do Java 5 ao invés de utilizar métodos com nomes hardcode, tais como: setUp, tearDown, test. Já o framework DBUnit é uma extensão do JUnit, e como o nome sugere, serve para testes que envolvem base de dados.
Esses tipos de testes são complicados, pois, como cumprir a regra de que um método de teste não pode depender do estado da base de dados? Imagine que deve-se testar uma rotina de faturamento, como o método de teste vai fazer para cadastrar um cliente, produto, tabela de preços, condições de pagamento, etc. Esse método de testes deveria ser capaz de fazer isso em uma única linha de código, tal como executar um script SQL para deixar a base pronta para a execução do teste. Mas será que um script SQL (gravado em um txt por exemplo) é uma idéia boa? e se a aplicação for independente de SGBD, pode ser que um script SQL comprometa demais a portabilidade. Com o DBUnit, ao invés de povoar as tabelas a partir de um script SQL, utiliza-se documentos XML para definição dos dados a serem povoados. Com esta funcionalidade, é possível cumprir todas as regras citadas anteriormente.

Criando uma classe base para os testes

Para dar suporte às classes concretas de testes (Unit Test Cases), criaremos uma classe abstrata para abstrair algumas tarefas comuns a todos os métodos de teste. Segue o código desta classe:

/**
* Classe base para os testes do sistema.
* @author Germano
* @version 1.0.0.0
*/
public abstract class MyAppTestCase {

protected TransactionManager transactionManager = new TransactionManager(HibernateUtilTDD.getSession());

/**
* Conexão com o banco de dados.
*/
private static IDatabaseTester databaseTester;

/**
* Cria um novo MyAppTestCase

* Configura o acesso a base de dados e as operações de SetUp e TearDown
*/
public MyAppTestCase() {
databaseTester = new JdbcDatabaseTester("com.mysql.jdbc.Driver",
"jdbc:mysql://localhost/myApp_tdd",
"myUser",
"myPass");

databaseTester.setTearDownOperation(DatabaseOperation.NONE);
}

/**
* Método a ser executado depois que todas as unidades de testes forem executadas.

* Responsável por realizar a desconexão com o banco de dados.
* @throws Exception
*/
@AfterClass
public static void dropDBConnection() throws Exception {
databaseTester.closeConnection(databaseTester.getConnection());
}

@Before
public void setUp() {
transactionManager.beginTransaction();
}

@After
public void tearDown() {
transactionManager.commit();
}

/**
* Coloca um novo conjunto de dados na base de dados para ser utilizado na unidade de testes.

* Obs.: Não limpa a base de dados.
* @param xmlDataSet
* @throws Exception
*/
protected void appendDataSet(String xmlDataSet) throws Exception {
IDataSet dataSet = new FlatXmlDataSet(new FileInputStream(getResourcesFolderFromTestUnit(xmlDataSet)));
databaseTester.setDataSet(dataSet);

databaseTester.setSetUpOperation(DatabaseOperation.INSERT);
databaseTester.onSetup();
}

/**
* Atualiza os registros do conjunto de dados atual.

* Obs.: Não remove os registros já cadastrados no conjunto de dados atual.
* @param xmlDataSet
* @throws Exception
*/
protected void updateDataSet(String xmlDataSet) throws Exception {
IDataSet dataSet = new FlatXmlDataSet(new FileInputStream(getResourcesFolderFromTestUnit(xmlDataSet)));
databaseTester.setDataSet(dataSet);

databaseTester.setSetUpOperation(DatabaseOperation.UPDATE);
databaseTester.onSetup();
}

/**
* Remove registros do conjunto de dados atual.

* Obs.: Remove somente os registros contidos no conjunto de dados informado no xmlDataSet.
* @param xmlDataSet
* @throws Exception
*/
protected void deleteDataSet(String xmlDataSet) throws Exception {
IDataSet dataSet = new FlatXmlDataSet(new FileInputStream(getResourcesFolderFromTestUnit(xmlDataSet)));
databaseTester.setDataSet(dataSet);

databaseTester.setSetUpOperation(DatabaseOperation.UPDATE);
databaseTester.onSetup();
}

/**
* Coloca um novo conjunto de dados a ser utilizado na unidade de testes.

* Obs.: Limpa a base de dados antes de inserir o novo conjunto de dados.
* @param xmlDataSet
* @throws Exception
*/
protected void setNewDataSet(String xmlDataSet) throws Exception {
// dataset vazio para limpar a base de dados.
IDataSet dataSet = new FlatXmlDataSet(new FileInputStream(getResourcesFolderFromUtil("empty-database.xml")));
databaseTester.setDataSet(dataSet);
databaseTester.setSetUpOperation(DatabaseOperation.DELETE_ALL);
databaseTester.onSetup();

// novo dataset definido via XML.
dataSet = new FlatXmlDataSet(new FileInputStream(getResourcesFolderFromTestUnit(xmlDataSet)));
databaseTester.setDataSet(dataSet);
databaseTester.setSetUpOperation(DatabaseOperation.CLEAN_INSERT);
databaseTester.onSetup();
}

/**
* Obtem o diretório de resources referente ao método de
* testes que invocou o método de definir DataSet.
* @return String
*/
private String getResourcesFolderFromTestUnit(String rsrc) {
StringBuffer packageName = new StringBuffer();

// obtem a pilha de chamada de métodos (um pouco de Reflection)
Throwable t = new Throwable();
String fullClassName = t.getStackTrace()[2].getClassName();

// através do nome completo da classe (pacote.class_name) extrai a string que
// representa o diretório onde se encontram os resources.
String[] folders = fullClassName.replace(".", ">").split(">");
for (int i = 0; i < folders.length-1; i++) {
packageName.append(folders[i]);
packageName.append("/");
}

URL rsrcUrl = MyAppTestCase.class.getClassLoader().getResource(packageName.toString() + rsrc);
return rsrcUrl.toString().substring(6);
}

/**
* Obtem o diretório de resources do pacote Utils dos testes.
* @return String
*/
private String getResourcesFolderFromUtil(String rsrc) {
// obtem a pilha de chamada de métodos (um pouco de Reflection++)
Throwable t = new Throwable();
String fullClassName = "";
try {
String className = t.getStackTrace()[2].getClassName();
fullClassName = Class.forName(className).getSuperclass().getCanonicalName();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

// através do nome completo da classe (pacote.class_name) extrai a string que
// representa o diretório onde se encontram os resources.
StringBuffer packageName = new StringBuffer();
String[] folders = fullClassName.replace(".", ">").split(">");
for (int i = 0; i < folders.length-1; i++) {
packageName.append(folders[i]);
packageName.append("/");
}

URL rsrcUrl = MyAppTestCase.class.getClassLoader().getResource(packageName.toString() + rsrc);
return rsrcUrl.toString().substring(6);
}
}


TransactionManager é uma simples classe que encapsula os tratamentos de transação, para isso ela deve receber no construtor uma instância da sessão do Hibernate.
IDatabaseTester é a interface do DBUnit.

Conteúdo do arquivo empty-database.xml.
Obs: Os arquivos XML usados nos testes devem ser colocados no mesmo diretório (pacote) da classe do teste em questão. Para quem utiliza o Eclipse, uma prática comum é criar dois source folders, um para os códigos Java e outro para os resources, sendo que ambos apontem para o mesmo diretório de output.

Exemplo bem simples de um Unit Test Case utilizando esta classe base:

/**
* Unidade de casos de testes de Modelo Corporativo.
* @author Germano
* @version 1.0.0.0
*/
public class ModeloCorporativoTestCase extends MyAppTestCase {

private NucleoBusinessFactory factory;

public ModeloCorporativoTestCase() {
super();
factory = new NucleoBusinessFactory(DaoFactory.getInstance(transactionManager.getSession()));
}

@Test
public void testAdiciona() throws Exception {
// adiciona um usuário à base
setNewDataSet("modeloCorporativoAdiciona.xml");

// obtem o usuário de código 1
Usuario u = factory.getUsuarioBusiness().retorna(1L);

ModeloCorporativo m = new ModeloCorporativo();
m.setCodigo("M01");
m.setDescricao("Empresas SUL");
m.setAbreviatura("M01 - EmpSul");
m.setUsuarioCadastro(u);
m.setMascara("11.22.333");

ModeloCorporativoBusiness business = factory.getModeloCorporativoBusiness();
business.adiciona(m);
assertTrue("1 - Não adicionou o modelo corporativo", (m.getId() != 0));
}
}


Conteúdo do arquivo modeloCorporativoAdiciona.xml.

Conclusão

Dentre todas as práticas sugeridas pelas metodologias de desenvolvimento ágil, com certeza o uso de testes (principalmente usando o paradigma de Test Driven Development) é uma das mais praticadas em projetos de desenvolvimento de software. Podendo contar com poderosas ferramentas de testes, como as citadas neste artigo e outras, a produtividade da equipe de testes (ou dos próprios programadores de sistema) torna-se cada vez maior.

Um comentário:

Bruna. disse...

Puxa, estou orgulhosa de você! O blog está nota 10!