jUnit: testando classe que mexem com arquivos

Testando classes que lidam com arquivos com jUnit Rules e TemporaryFolder

É quase que mandatório todo projeto Java ter uma classe FileUtils da vida para manipular arquivos… É ou não é?

public class FileUtils {

    /**
     * Lista todos os arquivos de um diretorio
     */
    public static List<File> lista(File diretorio) {
        File[] arquivos = diretorio.listFiles();
        return Arrays.asList(arquivos);
    }

    public static void deleta(File arquivo) { ... }

    public static void copia(File origem, File destino) { ... }

    public static void escreve(File arquivo, String conteudo) { ... }

    // outros métodos
}

Mas infelizmente ela não é levada tão a sério assim! No geral ela é testada manualmente“por tabela” quando testamos outra funcionalidade do sistema.

@Repository
public UsuarioDao {
    
    @Transactional
    public void deleta(Usuario usuario) {
        this.entityManager.remove(usuario);
        FileUtils.deleta(usuario.getFoto()); // deleta foto do disco
    }
}

Essa prática leva a problemas que você já deve conhecer bem, como brechas para bugs! O que fazer então? Simples, cubra a classe com testes automatizados!

Por azar nosso testar uma classe que acessa disco não é tão simples quanto um teste de unidade qualquer, por esse motivo existe alguns detalhes que você precisa saber antes de programar a 1a linha de teste (isso é muito importante!). Para entender estes detalhes, leia o novo post no blog daTriadWorks:

[Blog] Testes de Integração na prática: testando classes que manipulam arquivos com jUnit

De quebra você ainda conhece uma feature do jUnit que facilita em N vezes a escrita dos seus testes e principalmente a vida da equipe:

public class FileUtilsTest {

    @Rule
    public TemporaryFolder temp = new TemporaryFolder();

}

E aí, o que achou da dica? Aproveita e compartilha esse post com seus amigos e sua equipe – dá para tirar boas discussões daí!

Novo Curso: TDD e Testes Automatizados com Java

É com muito prazer que venho anunciar o mais novo curso da TriadWorks: TDD e Testes Automatizados com Java.

No curso você aprende a fazer TDD e escrever testes automatizados para sua aplicação!Você vai do 1o teste com jUnit, até TDD, testes de tela com Selenium WebDriver e build automatizado. Tudo em um curso enxuto e 100% prático com a didática diferenciada da TriadWorks que você conhece.

Para você ter idéia, este é o super resumo do conteúdo do nosso treinamento de TDD e testes em Java:
  1. Conscientização da importância dos testes;
  2. Testes de Unidade com jUnit;
  3. Mocks com o melhor framework do mercado: Mockito;
  4. Testes de Integração: teste seu SQL, DAO e acesso a banco de dados;
  5. Testes de ponta a ponta com Selenium WebDriver;
  6. TDD e refatoração contínua do seu código;
  7. Build Automatizado: compilando, testando e empacotando seu projeto;
  8. e muito mais…

Com instrutores com +7 anos de experiência fazendo TDD e testes automatizados com Java, nós temos uma boa idéia do que você precisa aprender para sair da inércia e escrever suas primeiras linhas de testes e ainda convencer sua equipe a adotá-los no projeto…

Falando nisso, estamos com descontos especiais para esta 1a turma! Clica no link abaixo, veja todo o conteúdo programático e faz tua inscrição:

>> Conheça o Curso TDD e Testes Automatizados com Java

Ah! Aproveito para te pedir um grande favor…Se você tiver um ou dois amigos que possam se beneficiar deste curso, por favor, me ajuda a divulgá-lo para eles! Tenho certeza que o investimento no aprendizado de testes trará retorno para vida profissional deles.

Então, o que achou do curso e do conteúdo programático?

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?