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?

14 thoughts on “Testes isolados com jUnit Rules

  1. Ótimo post!
    Estou começando a estudar a parte de testes unitários por agora, e estou motivado em começar a aplicar testes em todo o meu código, só me restou uma dúvida após ler este texto, ali diz que:
    um teste não é um teste de unidade se: Ele conversa com o banco de dados;

    Fiquei meio confuso com isso, poderia dar algum exemplo de como fazer um simples teste para verificar se o login e senha de um usuário é valido? sem consultar o banco?

    Obrigado! E continue com o ótimo trabalho! =)

    • Olá Pestano,

      Exemplo perfeito para utilizar o jUnit Rules! Eu já havia visto um exemplo desse tipo tempos atrás, até guardei o código ou a idéia dele em um gist. É possível fazer muita coisa com ele, é uma feature bastante poderosa!

      No mais, não entendi se no final o seu @Rule deu certo ou não!

      • Opa Ponte,
        funcionou sim, o problema que tive foi com o arquillian quando executo o teste em um ambiente remoto(o nosso build roda no servidor de integração continua mas os testes funcionais rodam em outra máquina) fora isso funciona que é uma beleza.

        abraço!

        • Perfeito!

          Já faz algum tempo que não uso testes de aceitação com Selenium/Webdriver, mas com certeza se fosse usar hoje eu implementaria essa Rule para tirar screenshots em caso de erro. Isso ajuda demais quando os testes começam a quebrar, principalmente pela fragilidade dos testes de aceitação.

          Bem que você poderia blogar sobre a solução no seu blog, tenho certeza que vai ajudar muita gente! :-)

  2. Rafael! Parabéns pelo post.
    Estamos tentando aplicar os testes unitários aqui na empresa, mas estamos com diversas dúvidas de como aplicar esses testes. Todas as nossas regras de negócio são feitas através de acesso a banco de dados. Como podemos proceder nesses casos?

    Percebi que os testes que estamos fazendo na verdade são testes de integração. É correto fazer somente testes de integração?

    Os objetos que utilizamos são objetos que dependem de muitos dados do banco, inicialmente pensamos que setar valores como por exemplo “id” diretamente no teste estava incorreto, pois não temos como garantir que esses dados vão realmente existir. A nossa ideia inicial era salvar um registro, utilizá-lo no teste e em seguida removê-lo. Qual a melhor abordagem para esses casos?

    Estou procurando por projetos semelhantes ao nosso que usam testes unitários para ver como o pessoal faz, mas está difícil de encontrar. Os testes que costumo ver são bem simples e fogem da nossa realidade.

    Desde já, agradeço pela atenção.

  3. Oi Fagner,

    É muito comum que a maioria dos projetos sejam “database-based” e muito das regras sempre tenham alguma relação com persistência. Não há qualquer problema em se ter apenas testes de integração, mas o ideal é ter um equilíbrio entre testes de unidade e integração.

    Para fazer testes de integração que acessem o banco, é interessante que cada teste limpe e popule as tabelas necessárias antes de rodar. Você pode fazer isso da maneira que achar conveniente, seja via scripts SQL, JDBC, Hibernate etc. Mas aconselho fortemente a usar o framework DbUnit, ele com certeza é hoje em dia a melhor alternativa.

    Em todos os nossos projetos nós usamos o DbUnit para limpar e popular as tabelas. Tanto é que, acabamos criando um projeto opensource para facilitar o uso do DbUnit: https://github.com/rponte/dbunitmanager (é uma API bem simples, apenas para encapsular detalhes do DBUnit).

    Eu tenho um projeto em JSF 1.2 na qual usa Spring e JPA/Hibernate, onde os testes de integração são feitos com o módulo Spring-Testing, além disso usamos DbUnit para popular o banco: https://github.com/rponte/jsf-loja-project

    Tem também este outro, com uma arquitetural semelhante mas um pouco mais atual, na qual usa VRaptor3, Spring e JPA/Hibernate e usa o DbUnitManager: https://github.com/triadworks/vraptor-blank-project/

    Ambos tem exemplos de classes de testes de integração usando DbUnit e Spring. Se você não usa Spring, você pode desconsiderá-lo nos testes e dar new direto nas suas classes pasando as devidas dependências. O Spring nos projetos só ajuda por aproximar o ambiente dos testes da produção, mas não é de fato necessário!

    No mais, acredito que este curso online do Alura possa te ajudar a escrever testes de integração: http://www.alura.com.br/cursos-online-agile/teste-de-integracao (existem outros cursos lá EXCELENTES sobre testes automatizados, aconselho que você faça todos).

    Acho que falei demais, espero que te ajude de alguma forma. Precisando de mais alguma coisa, é só falar.

  4. Olá Rafael,
    obrigado pela resposta e pelas dicas.
    Vou dar uma olhada no dbunit e nesse curso da Alura.

Leave a Reply to rmpestano Cancel reply

Your email address will not be published. Required fields are marked *