Testes isolados com jUnit Rules

Sempre houve uma discussão na comunidade sobre o que difere um teste de unidade de um teste de integração. Muitos consideram que um teste de unidade pode tocar o sistema de arquivos ou banco de dados; outros que um teste de unidade não deve levar mais do que 1 segundo para rodar; já outros consideram que testes de integração são testes que sempre tocam o banco de dados.

Alguns autores já tentaram definir o que um teste deve ter para ser considerado um teste de unidade. Outros autores, no entanto, foram mais além e tentaram definir o oposto, ou seja, o que um teste de unidade não é. Michael Feathers, por exemplo, listou 5 características que um teste de unidade não deve ter. Basicamente o que ele diz é que um teste não é um teste de unidade se:

  • Ele conversa com o banco de dados;
  • Ele se comunica através da rede;
  • Ele toca o sistema de arquivos;
  • Se ele não pode ser executado ao mesmo tempo com outros testes de unidade;
  • Se você tiver que configurar ou preparar o ambiente (como editar arquivos de configuração) para executar o teste;

Seguir os preceitos acima traz diversas vantagens na hora de escrever suas classes de teste, como testes de unidade mais rápidos, melhor feedback e rastreabilidade em caso de erros e claro, testes isolados e independentes.

Um teste de unidade só deveria ter um motivo para falhar, e este motivo é sua validação, seu assert. Quanto menos isolado um teste é, maiores são suas chances de falhar, e pior, por motivos algumas vezes difíceis de determinar.

Um teste de unidade deve rodar de forma isolada do ambiente externo e dos outros testes, isto é, o que um teste de unidade faz ou deixa de fazer não deveria influenciar na assertividade dos testes que executarão depois dele. Ter testes de unidade interdependentes é um problema e uma má prática.

Um forte indício para avaliar se seus testes de unidade foram escritos de maneira isolada é se a ordem em que eles rodam influencia o resultado final da bateria. Se os testes precisam ser executados em uma ordem específica para passarem, provavelmente, eles não estão bem isolados e independentes.

Se sujou, limpe

Mesmo testes de unidade aparentemente simples podem sofrer influências externas se não tomarmos os devidos cuidados. Um teste inofensivo para uma classe que formata uma data é um bom exemplo disso:

public class FormatadorDeData {

	public String formata(Date data) {
		SimpleDateFormat sdf  = new SimpleDateFormat("dd 'de' MMMM");
		return sdf.format(data);
	}
}

E sua classe de teste para verificar seu comportamento:

public class FormatadorDeDataTest {

	@Test
	public void deveFormatarDataParaRelatorio() {

		FormatadorDeData formatador = new FormatadorDeData();
		String dataFormatada = formatador.formata(data("23/12/2013"));

		assertEquals("23 de Dezembro", dataFormatada);
	}

	// outros métodos de teste
}

Se o desenvolvedor que escreveu o teste acima rodá-lo em um ambiente com a localização “pt_BR” o resultado será o esperado e o teste passará. Contudo, ao integrar o código no servidor de integração contínua (CI), que roda em outra localização, como “en_US”, o teste quebrará sem dúvida.

O problema é que a classe SimpleDateFormat usa o Locale padrão da JVM quando não informamos qual iremos utilizar. Como a JVM usa o locale do OS então temos uma dependência com o ambiente externo, pois o teste só passaria se rodassemos ele em um ambiente pré-configurado.

Levando em conta que nosso código não precisa ser internacionalizável, a correção é simples, basta informar o Locale “pt_BR” ao instanciar a classe SimpleDateFormat.

public class FormatadorDeData {
	public String formata(Date data) {
		SimpleDateFormat sdf  = new SimpleDateFormat("dd 'de' MMMM", new Locale("pt", "BR"));
		return sdf.format(data);
	}
}

Sabemos que apenas corrigir o problema não é suficiente, precisamos garantir que ele não volte a acontecer. E conseguimos isso escrevendo mais testes para nossa classe de produção. Podemos melhorar nossa classe de testes com o uso da anotação @Before do jUnit.

public class FormatadorDeDataTest {

	@Before
	public void setup() {
		Locale.setDefault(new Locale("en", "US"));
	}

	@Test
	public void deveFormatarDataParaRelatorio() {

		FormatadorDeData formatador = new FormatadorDeData();
		String dataFormatada = formatador.formata(data("23/12/2013"));

		assertEquals("23 de Dezembro", dataFormatada);
	}

	// outros métodos de teste
}

O método com @Before irá executar antes de cada método de teste (@Test), ou seja, todos os métodos de teste da classe executarão com o locale “en_US”, se todos os métodos passarem então temos certeza que a JVM não interfere na execução da classe FormatadorDeData.

Resolvemos um problema, mas acabamos criando outro. Alteramos o locale da JVM, que é uma configuração global, e todas as outras classes de teste rodarão com o locale que definimos. A priori isto não parece um problema, mas poderá quebrar outros testes que não esperavam por isso. É uma sujeira que deixamos no caminho e precisamos limpa-la.

Para que um teste rode de forma isolada, ele deve iniciar sempre em um estado limpo e válido, e com seu término, ele deve sempre desfazer qualquer sujeira que ele tenha deixado no caminho. A sujeira pode ser desde uma variável de ambiente ou da JVM, um arquivo ou diretório no sistema de arquivos, recursos abertos do OS, entre outros.

Novamente, o jUnit nos ajuda com a anotação @After, que é análoga ao @Before. Com ela podemos voltar o locale da JVM para seu estado inicial ao fim de cada teste. Teríamos um código semelhante a este:

public class FormatadorDeDataTest {

	private static final Locale localeOriginal = Locale.getDefault();

	@Before
	public void setup() {
		Locale.setDefault(new Locale("en", "US"));
	}

	@After
	public void cleanUp() {
		Locale.setDefault(localeOriginal);
	}

	// todo os métodos de teste
}

A solução acima é bastante simples, porém seu reuso é tão restrito quanto sua simplicidade. Para reusa-la noutras classes teríamos que repetir o código – e já sabemos que repetição de código nunca é uma coisa boa.

jUnit Rules

A partir do jUnit 4.7 foi adicionado um conceito chamado Rules, ele é muito semelhante aos runners customizados (@RunWith), porém são mais simples, mais flexíveis e menos limitados. A grosso modo, você usa a anotação @Rule em uma classe para indicar que deseja executar algum comportamento antes ou depois de cada método de teste (@Test), muito semelhante a um interceptor.

Para escrevermos nossa própria Rule, basta criarmos uma classe que implemente a interface TestRule. Ela possui um contrato bem simples:

public interface TestRule {
	public Statement apply(Statement base, Description description);
}

Tomando o problema do locale anterior, poderíamos resolvê-lo da seguinte forma:

import java.util.Locale;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

public class LocaleRule implements TestRule {

	private static final Locale defaultLocale = Locale.getDefault();
	private final Locale locale;

	public LocaleRule(Locale locale) {
		this.locale = locale;
	}

	@Override
	public Statement apply(final Statement base, Description description) {
		return new Statement() {
			@Override
			public void evaluate() throws Throwable {
				Locale.setDefault(locale); // altera o locale padrão
				try {
					base.evaluate(); // executa o teste
				} finally {
					Locale.setDefault(defaultLocale); // volta o locale inicial
				}
			}
		};
	}
}

Nossa classe de teste só precisaria declarar um atributo público e anota-lo com @Rule. A classe de teste teria o código abaixo:

import org.junit.Rule;

public class FormatadorDeDataTest {

	@Rule
	public LocaleRule locale = new LocaleRule(new Locale("en", "US"));

	@Test
	public void deveFormatarDataParaRelatorio() {

		FormatadorDeData formatador = new FormatadorDeData();
		String dataFormatada = formatador.formata(data("23/12/2013"));

		assertEquals("23 de Dezembro", dataFormatada);
	}

	// outros métodos de teste
}

O código além de mais simples e limpo, nos permite reusar a classe LocaleRule em outros testes facilmente. O código pode ainda ser melhorado se estendermos a classe ExternalResource, na qual foi desenhada para iniciar e terminar recursos externos relacionados a sistema de arquivos, sockets, conexões de banco etc.

Já existem algumas Rules prontas que vem com jUnit 4.10. Como por exemplo, rule para criar (e deletar) arquivos e diretórios temporários, para tratamento mais elegante de exceptions, para timeout, entre outras. Além disso, é possível rodar várias Rules juntas na mesma classe de teste, o que nos dá maior flexibilidade e reuso de código – algo que não é possível com @RunWith.

A funcionalidade Rules do jUnit é muito poderosa, ela nos permite executar cada método de teste dentro de um contexto isolado, dentro de sua própria sandbox. Com ela é possível criar contextos do Spring, preparar o EntityManager da JPA ou uma conexão JDBC para os testes de integração, fazer mocking e injeção de dependência (DI), levantar servidores embarcados e muitos mais.

Concluindo

Existem diversas práticas para manter os testes de unidade isolados e independentes, que vão desde o uso de mocks e stubs, preparar objetos e variáveis globais até o controle de onde arquivos e diretórios são criados. O ideal é escrevermos cada método de teste como se ele rodasse em seu próprio contexto, sua própria sandbox, dessa forma evitamos a interdependência entre os testes.

Cuidar do código de teste é tão importante quanto cuidar do código de produção, com jUnit Rules podemos escrever testes mais simples, menos fragéis e ainda por cima ganharmos reusabilidade de código.

Testes mal isolados trazem problemas que levam bastante tempo para resolver, pois nem sempre é fácil detectar a causa do problema, já que na maioria das vezes o teste passa quando executado sozinho, mas quebra quando executado junto com a bateria em uma máquina especifica e, até quem sabe, em uma determinada data ou horário.

Se cada desenvolvedor da equipe se preocupar em isolar a classe de teste que ele escreve adequadamente, limpando o que o teste suja, com certeza a bateria de testes do projeto rodará verde por muito mais tempo.

E aí, você já teve algum problema com testes interdependentes? Já conhecia ou já usou o jUnit Rules?

Maré de Agilidade Belo Horizonte: venha surfar nesta onda

A edição mineira do evento Maré de Agilidade, que acontece de 20 a 22 de maio em Belo Horizonte, superou as melhores expectativas dos seus coordenadores e preencheu, em pouco mais de uma semana, todas as 200 vagas oferecidas. A solução encontrada para atender a toda esta demanda reprimida, foi oferecer a opção do streaming ao vivo, no dia 22, como opção para todos aqueles que não conseguiram fazer sua inscrição para participar presencialmente do evento nos dias 20 e 21. As inscrições para o streaming ao vivo já estão abertas e podem ser feitas até o próximo dia 15 de maio, diretamente pelo site www.maredeagilidade.com.br. O pagamento é feito por meio de boleto bancário. O valor para inscrições feitas em abril é de R$ 30,00 e para as feitas em maio, de R$ 50,00.

Esta será a 5ª  edição deste evento iitinerante, que já passou por Brasília, Salvador, Fortaleza e Belém. O objetivo do Maré de Agillidades é disseminar o conhecimento de metodologias ágeis de desenvolvimento, promover networking, criar parcerias, fortalecer a empregabilidade e gerar negócios. Os mini-cursos, workshops e palestras serão ministrados nos dias 20 e 21 na unidade Pitágoras do Bairro Cidade Jardim (Rua Madalena Sofia, 30, no Shopping Jardim, e no dia 22, no pólo presencial da Unopar virtual, na Avenida Alfredo Camaratti, 121 – Pampulha.

Responsabilidade Social

Esta 5ª edição do Maré de Agilidade vai doar toda da renda obtida com as inscrições do evento (descontadas as despesas) para o Centro Espírita Bezerra de Menezes, de Pedro Leopoldo (MG), em homenagem ao centenário de nascimento de Chico Xavier.

Fundado em junho de 1962, a entidade presta vários serviços de utilidade pública para populações carentes da cidade mineira de Pedro Leopoldo (MG), como assistência odontológica, alimentação, distribuição de agasalhos, cestas básicas, entre vários outros benefícios.

Streaming

Quem se inscrever no streamming terá acesso privilegiado aos seguintes conteúdos:

  • Pedro Valente – Yahoo – Product Owner na prática
  • Rodrigo Yoshima – Aspercom – Implantando Scrum – experiências de um Agile Coach
  • Guilherme Silveira – Caelum – Deploy Contínuo: pois integração contínua não basta
  • Heitor Horiz – AdaptWorks – Planejamento e estimativas em projetos ágeis.
  • Carlos Barbieri – Fumsoft MPS.BR com metodologias ágeis
  • Alexadre Gomes – Sea Tecnologia – Escolhas 2.0
  • Manoel Pimentel – Stefanini – Coaching e Facilitação de equipes ágeis
  • Renato Willi – Sea Tecnologia – Agilidade e Licitações

Para quem não foi ágil o bastante e ainda quer participar desta onda, não perca esta segunda oportunidade e inscreva-se no streaming ou junte-se ao pólo de transmissão mais perto de você. Mais informações no site: www.maredeagilidade.com.br.