JUnit Categories: uma abordagem prática

Categorias

Introdução

Este post é um caso de uso de uma situação prática que vivenciei recentemente na empresa onde trabalho e como utilizei as JUnit Categories como parte da solução. Vou descrever o cenário que vivíamos e como encontramos uma solução que, momentaneamente, atende-nos. Vamos ao caso.

O cenário existente

Trabalho em um grande órgão da administração pública federal. Dentro desse órgão, faço parte do principal sistema de software da casa. É um sistema web e que possui uma base de código de cerca de 10 (dez) anos. Não vou entrar em detalhes técnicos de sua implementação, mas, na era dos microservices, ainda somos um grande monolito.

Uma coisa que nos pertuba bastante é a qualidade do sistema entregue. Como o sistema é muito grande, com milhares de linhas de código, vez por outra sofremos com erros de regressão. A prática de testes de unidade ainda não faz parte da cultura de toda a equipe, mas existem testes desse tipo e que nos ajudam bastante – a propósito, estamos num momento em que estamos ampliando a bateria desses tipos de teste.

Mas para evitar os erros de regressão percebidos pelos usuários, precisávamos de testes que se aproximem daquilo que o usuário faz: precisávamos de testes de aceitação automatizados! O conhecimento técnico para escrita desses testes já possuíamos – inclusive já tínhamos alguns desses testes escritos, mas ainda faltava a estratégia para automatizá-los de verdade.

Como deveria ser…

Bem, no cenário ideal, o que deveríamos fazer era implementar um pipeline de entrega contínua, como descrito no livro Continuous Delivery e apresentado na figura abaixo.

Pipeline de Entrega Contínua

De forma resumida, o pipeline é uma série de “passos” que são executados desde o commit do código pelo desenvolvedor no sistema controlador de versão (CVS, SVN, Git etc.) até o release efetivo, ou seja, deployment em produção. Assim, cada passo é disparado pelo passo anterior:

  1. Após o commit do desenvolvedor, é disparado o build da aplicação e execução dos testes de unidade; se o build ou a execução dos testes de unidade falhar (vermelho), o processo pára e o desenvolvedor responsável pela mudança (commit) é avisado do problema (feedback);
  2. Se o passo anterior passar (verde), é disparada a execução dos testes de aceitação automatizados; se eles falharem, o processo pára e é realizado o feedback para o desenvolvedor.
  3. E o processo continua, em caso de sucesso, até o deployment de mudança no ambiente de produção.

Bem esse é o cenário ideal, que deveríamos ter adotado, mas a história, pelo menos pra gente, (ainda) não é bem assim.

Por que não adotamos o pipeline de entrega contínua?

Bem, aqui eu vou contar a parte da história da qual não me orgulho muito, mas é a história como ela é. A verdade é que optamos nesse momento (e ainda não sabemos até quando) por não adotar o pipeline de entrega contínua, devido alguns motivos descritos a seguir.

1. Dificuldade de automação de alguns passos

Para implementar fielmente o pipeline de entrega contínua, todo o processo precisa ser automatizado.

Atualmente, temos a parte inicial do processo totalmente automatizada: após o commit do desenvolvedor, o nosso Jenkins (servidor de integração contínua) baixa o projeto diretamente do sistema de controle de versões, compila todo o projeto e dispara a execução de nossa suite de testes de unidade. Se os testes falharem, o desenvolvedor responsável é avisado do problema para tomar as providências.

Mas paramos por aí. Não temos automatizado o passo dos testes de aceitação. Esse passo envolve alguns desafios.

Após o sucesso da etapa de testes de unidade, faz-se necessária a geração do pacote para a implantação no servidor de aplicação para que os testes de aceitação automatizados sejam executados. A geração do pacote é tranquila – uma task do Maven faz isso com os pés nas costas. O problema é a outra parte.

Para a outra parte funcionar, precisamos fazer com o que o Jenkins seja capaz de subir um servidor de aplicação, fazer o deploy do pacote dentro do mesmo e, por fim, executar os testes de aceitação. Nossa dificuldade, hoje, reside na subida do servidor de aplicação. Estamos estudando soluções baseadas em containers Docker, mas ainda não temos isso pronto, o que inviabiliza, por si só, o uso do pipeline de entrega contínua da forma ideal.

2. Rapidez de feedback

Um outro problema advém da natureza de nossa aplicação. Hoje, a nossa aplicação é um grande monolito. Com isso, dentre outros problemas, o processo de deployment é lento. No pipeline de entrega contínua,  após os testes de unidade viria o deployment e a execução dos testes de aceitação que, naturalmente, também não são de rápida execução. E imaginamos que vamos ter algumas dezenas de testes desse tipo para todo o sistema.

Ou seja o tempo de resposta para o desenvolvedor, no cenário que vivenciamos, seria muito alto. O desenvolvedor teria que esperar alguns minutos até saber se a sua mudança não ocasionou problemas no sistema. Isso, ao longo prazo, é inviável é impraticável. Um dos objetivos de integração e entrega e contínua e rapidez de feedback.

A solução para esse problema é um melhor particionamento do sistema: precisamos modularizar melhor o mesmo para ter, em um processo de entrega contínua, um feedback aceitável. Mas o ponto é que ainda não vivemos esse cenário – dever de casa para nossa equipe.

3. Urgência de implantação do processo

Como citei no início do post, com alguma (indesejada) frequência o sistema sofre regressão. Aparecem erros, por vezes, simples e que seriam facilmente capturados com testes de aceitação automatizados.

Portanto tornou-se imperiosa a existência e execução desse tipo de teste. Não era mais possível que esperássemos a solução do nosso problema de automatizar o deployment para o nosso ambiente de testes de aceitação e muito menos esperar a reestruturação da aplicação para uma arquitetura mais modular.

O nosso problema era muito urgente e precisávamos de uma solução, mesmo que não fosse a ideal.

***

Isto posto, vamos a explicação técnica da solução que adotamos como uma forma de termos testes de aceitação automatizados de uma maneira minimamente aceitável e como utilizamos as JUnit Categories para nos ajudar nesta solução.

A nossa solução

A nossa aplicação possui, claramente, alguns grandes módulos – que ainda podem ser particionados em módulos menores. Tendo isso em vista, optamos por particionar os nossos testes de aceitação de acordo com esse módulos existentes.

Assim somos capazes de criar, no Jenkins, jobs de testes de aceitação isolados, que correspondem a cada um desses módulos. Com isso, mesmo sem ainda ter a modularização ideal dentro da aplicação, ganhamos em dois pontos:

  1. Escopo dos testes. Os testes ficam com escopos menores, associados aos módulos. Assim os testes de cada módulo rodam isolados uns dos outros.
  2. Rapidez. Com o escopo de cada teste associado a um módulo, os testes de cada módulo rodam mais rápido do que ocorreria se executássemos em conjunto com todos os testes da aplicação. Isso permite que o feedback ao desenvolvedor seja mais rápido.

Portanto o que fizemos foi categorizar nossos testes de aceitação. Como já utilizávamos o JUnit para escrever esses testes, nada mais natural do que utilizar as JUnit Categories.

JUnit Categories

No JUnit 4 foi adicionado o conceito de Category. Com isso, podemos organizar casos de testes em diferentes categorias e executá-los de forma agrupada.

Vou mostrar a seguir como fazer uso das categorias.

#1 Criando as Categorias

No JUnit, uma Category é uma interface de marcação – assim como a java.io.Serializable, por exemplo. Portanto precisamos criar essas interfaces marcadoras para representar cada uma de nossas categorias.

public interface DocumentoTests {}
public interface JudicialTests {}

Nos trechos de código acima criamos 2 interfaces que representam nossas categorias de testes de aceitação. O próximo passo é categorizarmos nossos testes com essas categorias.

#2 Utilizando as Categorias

Categorizar um caso de teste é muito simples. Basta utilizar a anotação @Category. Podemos utilizar essa anotação em métodos ou em classes. No nosso cenário, optamos por utilizá-la ao nível da classe.

@Category(DocumentoTests.class)
public class IncluirDocumentoAdministrativoTest {

 @Test
 public void deveIncluirMinutaDocumento() {
    // código do teste
 }

}

No trecho de código acima, marcamos os casos de teste da classe IncluirDocumentoAdministrativoTest com a categoria DocumentoTests. Com isso ele agora faz parte dessa categoria.

E como executar isso?

Há algumas formas. O JUnit oferece um runner, o Categories (sobre o qual você pode saber mais nesse link). Como nosso projeto utiliza o Maven, optamos por usá-lo para parametrizar a execução dos testes e criar os jobs no Jenkins.

Integrando com o Maven

Para a criação dos jobs no Jenkins utilizamos o conceito de Profiles do Maven. Para cada uma das categorias existentes, criamos um profile no nosso pom.xml o que nos permite a execução parametrizada dos testes de aceitação da seguinte forma, por exemplo.

mvn test -P documento

O argumento -P permite informar qual profile queremos executar.

Abaixo está parte do nosso pom.xml onde estão as configuração dos profiles.

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-surefire-plugin</artifactId>
				<version>2.12.2</version>
				<dependencies>
					<dependency>
						<groupId>org.apache.maven.surefire</groupId>
						<artifactId>surefire-junit47</artifactId>
						<version>2.12.2</version>
					</dependency>
				</dependencies>
				<configuration>
					<groups>${testcase.groups}</groups>
				</configuration>
			</plugin>
		</plugins>
	</build>

	<profiles>
		<profile>
			<id>documento</id>
			<properties>
				<testcase.groups>
					br.mp.mpf.unico.uitests.categories.DocumentoTests
				</testcase.groups>
			</properties>
		</profile>
		
		<profile>
			<id>judicial</id>
			<properties>
				<testcase.groups>
					br.mp.mpf.unico.uitests.categories.JudicialTests
				</testcase.groups>
			</properties>
		</profile>
	</profiles>

Observem os 2 trechos em destaque.

O primeiro trecho utiliza a configuração do plugin Maven Surefire (saiba mais aqui) que fornece suporte às JUnit Categories meio da tag groups. Informamos como valor a variável ${testcase.groups} – a definição do valor dessa variável vem no trecho seguinte.

O segundo trecho é a configuração dos profiles. Cada profile é configurado com uma propriedade testcase.groups, cujo valor é a categoria que criamos em nosso projeto (as interfaces marcadoras). Por exemplo o profile documento corresponde à categoria DocumentoTests.

Conclusão

Nesse post mostrei um uso prático das JUnit Categories como parte da solução do problema de automatização dos nossos testes de aceitação, utilizadas em conjunto com profiles Maven.

Como já citei, não é a solução ideal para esse tipo de problema, onde o mais certo seria a implementação do pipeline de entrega contínua.

De qualquer forma, penso que é uma solução aceitável e que pode ser implementada em alguns cenários onde a automação é difícil ou não pode ser feita totalmente.

Abraços e até a próxima.