Um guia para testes robustos de unidade e integração com JUnit

Publicados: 2022-03-11

Os testes de software automatizados são extremamente importantes para a qualidade, manutenção e extensibilidade de longo prazo dos projetos de software e, para Java, o JUnit é o caminho para a automação.

Embora a maior parte deste artigo se concentre em escrever testes de unidade robustos e utilizar stub, mocking e injeção de dependência, também discutiremos JUnit e testes de integração.

A estrutura de teste JUnit é uma ferramenta comum, gratuita e de código aberto para testar projetos baseados em Java.

No momento da redação deste artigo, JUnit 4 é o principal lançamento atual, tendo sido lançado há mais de 10 anos, com a última atualização há mais de dois anos.

JUnit 5 (com os modelos de programação e extensão Jupiter) está em desenvolvimento ativo. Ele suporta melhor os recursos de linguagem introduzidos no Java 8 e inclui outros recursos novos e interessantes. Algumas equipes podem achar o JUnit 5 pronto para uso, enquanto outros podem continuar usando o JUnit 4 até que o 5 seja lançado oficialmente. Veremos exemplos de ambos.

Executando JUnit

Os testes JUnit podem ser executados diretamente no IntelliJ, mas também podem ser executados em outros IDEs como Eclipse, NetBeans ou até mesmo na linha de comando.

Os testes devem sempre ser executados em tempo de compilação, especialmente os testes de unidade. Uma compilação com qualquer teste com falha deve ser considerada falha, independentemente de o problema estar na produção ou no código de teste – isso requer disciplina da equipe e disposição para dar prioridade máxima à resolução de testes com falha, mas é necessário aderir ao espírito de automação.

Os testes JUnit também podem ser executados e relatados por sistemas de integração contínua como o Jenkins. Projetos que usam ferramentas como Gradle, Maven ou Ant têm a vantagem adicional de poder executar testes como parte do processo de compilação.

Grupos de engrenagens indicando compatibilidade: JUnit 4 com NetBeans em um, JUnit 5 com Eclipse e Gradle em outro, e o último com JUnit 5 com Maven e IntelliJ IDEA.

Gradle

Como um projeto Gradle de amostra para JUnit 5, consulte a seção Gradle do guia do usuário JUnit e o repositório junit5-samples.git. Observe que ele também pode executar testes que usam a API JUnit 4 (referida como “vintage” ).

O projeto pode ser criado no IntelliJ através da opção de menu File > Open… > navegue até o junit-gradle-consumer sub-directory > OK > Open as Project > OK para importar o projeto do Gradle.

Para o Eclipse, o plug-in Buildship Gradle pode ser instalado em Help > Eclipse Marketplace… O projeto pode ser importado com File > Import… > Gradle > Gradle Project > Next > Next > Navegue até o junit-gradle-consumer > Next > Avançar > Concluir.

Depois de configurar o projeto Gradle no IntelliJ ou Eclipse, a execução da tarefa de build Gradle incluirá a execução de todos os testes JUnit com a tarefa de test . Observe que os testes podem ser ignorados em execuções subsequentes de build se nenhuma alteração for feita no código.

Para JUnit 4, veja o uso do JUnit com o Gradle wiki.

Especialista

Para JUnit 5, consulte a seção Maven do guia do usuário e o repositório junit5-samples.git para obter um exemplo de projeto Maven. Isso também pode executar testes antigos (aqueles que usam a API JUnit 4).

No IntelliJ, use File > Open… > navegue até junit-maven-consumer/pom.xml > OK > Open as Project. Os testes podem ser executados em Projetos Maven > junit5-maven-consumer > Ciclo de vida > Teste.

No Eclipse, use File > Import… > Maven > Existing Maven Projects > Next > Navegue até o junit-maven-consumer > Com o pom.xml selecionado > Finish.

Os testes podem ser executados executando o projeto como Maven build… > especifique o objetivo do test > Executar.

Para JUnit 4, consulte JUnit no repositório Maven.

Ambientes de Desenvolvimento

Além de executar testes por meio de ferramentas de compilação como Gradle ou Maven, muitos IDEs podem executar testes JUnit diretamente.

INtelliJ IDEA

O IntelliJ IDEA 2016.2 ou posterior é necessário para testes JUnit 5, enquanto os testes JUnit 4 devem funcionar em versões anteriores do IntelliJ.

Para os propósitos deste artigo, você pode querer criar um novo projeto no IntelliJ a partir de um dos meus repositórios do GitHub ( JUnit5IntelliJ.git ou JUnit4IntelliJ.git), que inclui todos os arquivos no exemplo simples da classe Person e usa o built-in bibliotecas JUnit. O teste pode ser executado com Executar > Executar 'Todos os Testes'. O teste também pode ser executado no IntelliJ da classe PersonTest .

Esses repositórios foram criados com novos projetos Java IntelliJ e constroem as estruturas de diretório src/main/java/com/example e src/test/java/com/example . O diretório src/main/java foi especificado como uma pasta de origem enquanto src/test/java foi especificado como uma pasta de origem de teste. Depois de criar a classe PersonTest com um método de teste anotado com @Test , ela pode falhar ao compilar, nesse caso o IntelliJ oferece a sugestão de adicionar JUnit 4 ou JUnit 5 ao caminho da classe que pode ser carregado da distribuição IntelliJ IDEA (veja estes respostas no Stack Overflow para mais detalhes). Finalmente, uma configuração de execução JUnit foi adicionada para Todos os Testes.

Consulte também as Diretrizes de instruções de teste do IntelliJ.

Eclipse

Um projeto Java vazio no Eclipse não terá um diretório raiz de teste. Isso foi adicionado em Propriedades do projeto > Caminho de construção Java > Adicionar pasta… > Criar nova pasta… > especifique o nome da pasta > Concluir. O novo diretório será selecionado como uma pasta de origem. Clique em OK nas duas caixas de diálogo restantes.

Testes JUnit 4 podem ser criados com Arquivo > Novo > Caso de Teste JUnit. Selecione “New JUnit 4 test” e a pasta de origem recém-criada para testes. Especifique uma “classe em teste” e um “pacote”, certificando-se de que o pacote corresponda à classe em teste. Em seguida, especifique um nome para a classe de teste. Depois de terminar o assistente, se solicitado, escolha “Adicionar biblioteca JUnit 4” ao caminho de construção. O projeto ou classe de teste individual pode ser executado como um teste JUnit. Consulte também Escrevendo e Executando Testes JUnit do Eclipse.

NetBeans

O NetBeans suporta apenas testes JUnit 4. As classes de teste podem ser criadas em um projeto Java do NetBeans com File > New File… > Unit Tests > JUnit Test ou Test for Existing Class. Por padrão, o diretório raiz de teste é denominado test no diretório do projeto.

Classe de Produção Simples e seu Caso de Teste JUnit

Vamos dar uma olhada em um exemplo simples de código de produção e seu código de teste de unidade correspondente para uma classe Person muito simples. Você pode baixar o código de exemplo do meu projeto github e abri-lo via IntelliJ.

src/main/java/com/example/Person.java
 package com.example; class Person { private final String givenName; private final String surname; Person(String givenName, String surname) { this.givenName = givenName; this.surname = surname; } String getDisplayName() { return surname + ", " + givenName; } }

A classe Person imutável tem um construtor e um método getDisplayName() . Queremos testar se getDisplayName() retorna o nome formatado como esperamos. Aqui está o código de teste para um teste de unidade única (JUnit 5):

src/test/java/com/example/PersonTest.java
 package com.example; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class PersonTest { @Test void testGetDisplayName() { Person person = new Person("Josh", "Hayden"); String displayName = person.getDisplayName(); assertEquals("Hayden, Josh", displayName); } }

PersonTest usa @Test e assertion do JUnit 5. Para JUnit 4, a classe e o método PersonTest precisam ser públicos e diferentes importações devem ser usadas. Aqui está o exemplo de Gist do JUnit 4.

Ao executar a classe PersonTest no IntelliJ, o teste é aprovado e os indicadores da interface do usuário ficam verdes.

Convenções comuns de JUnit

Nomeação

Embora não seja obrigatório, usamos convenções comuns ao nomear a classe de teste; especificamente, começamos com o nome da classe que está sendo testada ( Person ) e acrescentamos “Test” a ela ( PersonTest ). Nomear o método de teste é semelhante, começando com o método que está sendo testado ( getDisplayName() ) e acrescentando “test” a ele ( testGetDisplayName() ). Embora existam muitas outras convenções perfeitamente aceitáveis ​​para nomear métodos de teste, é importante ser consistente em toda a equipe e no projeto.

Nome em Produção Nome em teste
Pessoa Teste de pessoa
getDisplayName() testDisplayName()

Pacotes

Também empregamos a convenção de criar a classe PersonTest do código de teste no mesmo pacote ( com.example ) que a classe Person do código de produção. Se usássemos um pacote diferente para testes, seríamos obrigados a usar o modificador de acesso público em classes de código de produção, construtores e métodos referenciados por testes de unidade, mesmo quando não for apropriado, então é melhor mantê-los no mesmo pacote . No entanto, usamos diretórios de origem separados ( src/main/java e src/test/java ), pois geralmente não queremos incluir código de teste nas compilações de produção lançadas.

Estrutura e Anotação

A anotação @Test (JUnit 4/5) diz ao JUnit para executar o método testGetDisplayName() como um método de teste e relatar se ele passou ou falhou. Contanto que todas as asserções (se houver) passem e nenhuma exceção seja lançada, o teste é considerado aprovado.

Nosso código de teste segue o padrão de estrutura de Arrange-Act-Assert (AAA). Outros padrões comuns incluem Given-When-Then e Setup-Exercise-Verify-Teardown (Teardown normalmente não é explicitamente necessário para testes de unidade), mas usamos AAA neste artigo.

Vamos dar uma olhada em como nosso exemplo de teste segue o AAA. A primeira linha, o “arrange” cria um objeto Person que será testado:

 Person person = new Person("Josh", "Hayden");

A segunda linha, o “act”, exercita o método Person.getDisplayName() do código de produção:

 String displayName = person.getDisplayName();

A terceira linha, o “assert”, verifica se o resultado é o esperado.

 assertEquals("Hayden, Josh", displayName);

Internamente, a chamada assertEquals() usa o método equals do objeto String “Hayden, Josh” para verificar o valor real retornado das correspondências do código de produção ( displayName ). Se não correspondesse, o teste teria sido marcado como reprovado.

Observe que os testes geralmente têm mais de uma linha para cada uma dessas fases AAA.

Testes Unitários e Código de Produção

Agora que abordamos algumas convenções de teste, vamos voltar nossa atenção para tornar o código de produção testável.

Voltamos à nossa classe Person , onde implementei um método para retornar a idade de uma pessoa com base em sua data de nascimento. Os exemplos de código exigem o Java 8 para aproveitar as novas APIs de data e funcionais. Aqui está a aparência da nova classe Person.java :

Pessoa.java
 // ... class Person { // ... private final LocalDate dateOfBirth; Person(String givenName, String surname, LocalDate dateOfBirth) { // ... this.dateOfBirth = dateOfBirth; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, LocalDate.now()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }

A execução desta classe (no momento em que escrevo) anuncia que Joey tem 4 anos. Vamos adicionar um método de teste:

PersonTest.java
 // ... class PersonTest { // ... @Test void testGetAge() { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); long age = person.getAge(); assertEquals(4, age); } }

Ele passa hoje, mas e quando o executarmos daqui a um ano? Este teste é não determinístico e frágil, pois o resultado esperado depende da data atual do sistema executando o teste.

Stubbing e injetando um fornecedor de valor

Ao executar em produção, queremos usar a data atual, LocalDate.now() , para calcular a idade da pessoa, mas para fazer um teste determinístico mesmo daqui a um ano, os testes precisam fornecer seus próprios valores currentDate .

Isso é conhecido como injeção de dependência. Não queremos que nosso objeto Person determine a data atual em si, mas queremos passar essa lógica como uma dependência. Os testes de unidade usarão um valor conhecido e fragmentado, e o código de produção permitirá que o valor real seja fornecido pelo sistema em tempo de execução.

Vamos adicionar um fornecedor LocalDate a Person.java :

Pessoa.java
 // ... class Person { // ... private final LocalDate dateOfBirth; private final Supplier<LocalDate> currentDateSupplier; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier) { // ... this.dateOfBirth = dateOfBirth; this.currentDateSupplier = currentDateSupplier; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, currentDateSupplier.get()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }

Para facilitar o teste do método getAge() , nós o alteramos para usar currentDateSupplier , um fornecedor LocalDate , para recuperar a data atual. Se você não sabe o que é um fornecedor, recomendo ler sobre as interfaces funcionais integradas do Lambda.

Também adicionamos uma injeção de dependência: o novo construtor de testes permite que os testes forneçam seus próprios valores de data atuais. O construtor original chama esse novo construtor, passando uma referência de método estático de LocalDate::now , que fornece um objeto LocalDate , então nosso método principal ainda funciona como antes. E o nosso método de teste? Vamos atualizar PersonTest.java :

PersonTest.java
 // ... class PersonTest { // ... @Test void testGetAge() { LocalDate dateOfBirth = LocalDate.parse("2013-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-17"); Person person = new Person("Joey", "Doe", dateOfBirth, ()->currentDate); long age = person.getAge(); assertEquals(4, age); } }

O teste agora injeta seu próprio valor currentDate , então nosso teste ainda passará quando executado no próximo ano ou durante qualquer ano. Isso é comumente chamado de stub , ou fornecer um valor conhecido a ser retornado, mas primeiro tivemos que alterar Person para permitir que essa dependência fosse injetada.

Observe a sintaxe lambda ( ()->currentDate ) ao construir o objeto Person . Isso é tratado como um fornecedor de um LocalDate , conforme exigido pelo novo construtor.

Zombando e Stubbing um Web Service

Estamos prontos para que nosso objeto Person — cuja existência inteira está na memória da JVM — se comunique com o mundo exterior. Queremos adicionar dois métodos: o método publishAge() , que postará a idade atual da pessoa, e o método getThoseInCommon() , que retornará nomes de pessoas famosas que compartilham o mesmo aniversário ou têm a mesma idade que nossa Person . Suponha que haja um serviço RESTful com o qual podemos interagir chamado “Aniversários de Pessoas”. Temos um cliente Java para ele que consiste em uma única classe, BirthdaysClient .

com.example.birthdays.BirthdaysClient
 package com.example.birthdays; import java.io.IOException; import java.util.Arrays; import java.util.Collection; public class BirthdaysClient { public void publishRegularPersonAge(String name, long age) throws IOException { System.out.println("publishing " + name + "'s age: " + age); // HTTP POST with name and age and possibly throw an exception } public Collection<String> findFamousNamesOfAge(long age) throws IOException { System.out.println("finding famous names of age " + age); return Arrays.asList(/* HTTP GET with age and possibly throw an exception */); } public Collection<String> findFamousNamesBornOn(int month, int dayOfMonth) throws IOException { System.out.println("finding famous names born on day " + dayOfMonth + " of month " + month); return Arrays.asList(/* HTTP GET with month and day and possibly throw an exception */); } }

Vamos aprimorar nossa classe Person . Começamos adicionando um novo método de teste para o comportamento desejado de publishAge() . Por que começar com o teste, em vez da funcionalidade? Estamos seguindo os princípios do desenvolvimento orientado a testes (também conhecido como TDD), em que escrevemos primeiro o teste e depois o código para fazê-lo passar.

PersonTest.java
 // … class PersonTest { // … @Test void testPublishAge() { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate); person.publishAge(); } }

Neste ponto, o código de teste falha ao compilar porque não criamos o método publishAge() que ele está chamando. Uma vez que criamos um método Person.publishAge() vazio, tudo passa. Agora estamos prontos para o teste para verificar se a idade da pessoa realmente é publicada no BirthdaysClient .

Adicionando um objeto simulado

Como este é um teste de unidade, ele deve ser executado rapidamente e na memória, portanto, o teste construirá nosso objeto Person com um BirthdaysClient simulado para que ele não faça uma solicitação da web. O teste usará esse objeto simulado para verificar se ele foi chamado conforme o esperado. Para fazer isso, adicionaremos uma dependência no framework Mockito (licença MIT) para criar objetos simulados e, em seguida, criaremos um objeto BirthdaysClient simulado:

PersonTest.java
 // ... import com.example.birthdays.BirthdaysClient; // ... import static org.mockito.Mockito.mock; class PersonTest { private BirthdaysClient birthdaysClient = mock(BirthdaysClient.class); // ... @Test void testPublishAge() { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); // ... } }

Além disso, aumentamos a assinatura do construtor Person para receber um objeto BirthdaysClient e alteramos o teste para injetar o objeto BirthdaysClient simulado.

Adicionando uma expectativa simulada

Em seguida, adicionamos ao final de nosso testPublishAge uma expectativa de que o BirthdaysClient seja chamado. Person.publishAge() deve chamá-lo, conforme mostrado em nosso novo PersonTest.java :

PersonTest.java
 // ... class PersonTest { // ... @Test void testPublishAge() throws IOException { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); verifyZeroInteractions(birthdaysClient); person.publishAge(); verify(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); } }

Nosso BirthdaysClient aprimorado com Mockito mantém o controle de todas as chamadas que foram feitas para seus métodos, que é como verificamos se nenhuma chamada foi feita para BirthdaysClient com o método verifyZeroInteractions() antes de chamar publishAge() . Embora não seja necessário, ao fazer isso, garantimos que o construtor não está fazendo chamadas não autorizadas. Na linha verify() , especificamos como esperamos que a chamada para BirthdaysClient seja.

Observe que, como o publishRegularPersonAge tem a IOException em sua assinatura, também a adicionamos à assinatura do nosso método de teste.

Neste ponto, o teste falha:

 Wanted but not invoked: birthdaysClient.publishRegularPersonAge( "Joe Sixteen", 16L ); -> at com.example.PersonTest.testPublishAge(PersonTest.java:40)

Isso é esperado, já que ainda não implementamos as alterações necessárias em Person.java , já que estamos seguindo o desenvolvimento orientado a testes. Vamos agora fazer este teste passar fazendo as alterações necessárias:

Pessoa.java
 // ... class Person { // ... private final BirthdaysClient birthdaysClient; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now, new BirthdaysClient()); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier, BirthdaysClient birthdaysClient) { // ... this.birthdaysClient = birthdaysClient; } // ... void publishAge() { String nameToPublish = givenName + " " + surname; long age = getAge(); try { birthdaysClient.publishRegularPersonAge(nameToPublish, age); } catch (IOException e) { // TODO handle this! e.printStackTrace(); } } }

Teste de exceções

Fizemos o construtor do código de produção instanciar um novo BirthdaysClient , e publishAge() agora chama o birthdaysClient . Todos os testes passam; tudo é verde. Excelente! Mas observe que publishAge() está engolindo a IOException. Em vez de deixá-lo borbulhar, queremos envolvê-lo com nossa própria PersonException em um novo arquivo chamado PersonException.java :

PersonException.java
 package com.example; public class PersonException extends Exception { public PersonException(String message, Throwable cause) { super(message, cause); } }

Implementamos este cenário como um novo método de teste em PersonTest.java :

PersonTest.java
 // ... class PersonTest { // ... @Test void testPublishAge_IOException() throws IOException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); IOException ioException = new IOException(); doThrow(ioException).when(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); try { person.publishAge(); fail("expected exception not thrown"); } catch (PersonException e) { assertSame(ioException, e.getCause()); assertEquals("Failed to publish Joe Sixteen age 16", e.getMessage()); } } }

O Mockito doThrow() chama stubs birthdaysClient para lançar uma exceção quando o método publishRegularPersonAge() é chamado. Se a PersonException não for lançada, falharemos no teste. Caso contrário, afirmamos que a exceção foi devidamente encadeada com a IOException e verificamos se a mensagem de exceção é a esperada. No momento, como não implementamos nenhum tratamento em nosso código de produção, nosso teste falha porque a exceção esperada não foi lançada. Aqui está o que precisamos mudar em Person.java para fazer o teste passar:

Pessoa.java
 // ... class Person { // ... void publishAge() throws PersonException { // ... try { // ... } catch (IOException e) { throw new PersonException("Failed to publish " + nameToPublish + " age " + age, e); } } }

Stubs: Quando e Asserções

Agora implementamos o método Person.getThoseInCommon() , fazendo com que nossa classe Person.Java assim.

Nosso testGetThoseInCommon() , ao contrário de testPublishAge() , não verifica se chamadas específicas foram feitas para métodos birthdaysClient . Em vez disso, ele usa when chamadas para stub retornam valores para chamadas para findFamousNamesOfAge() e findFamousNamesBornOn() que getThoseInCommon() precisará fazer. Em seguida, afirmamos que todos os três nomes em stub que fornecemos são retornados.

Agrupar várias asserções com o assertAll() JUnit 5 permite que todas as asserções sejam verificadas como um todo, em vez de parar após a primeira asserção com falha. Também incluímos uma mensagem com assertTrue() para identificar nomes específicos que não estão incluídos. Aqui está a aparência do nosso método de teste de “caminho feliz” (um cenário ideal) (observe, este não é um conjunto robusto de testes por natureza de ser “caminho feliz”, mas falaremos sobre o porquê mais tarde.

PersonTest.java
 // ... class PersonTest { // ... @Test void testGetThoseInCommon() throws IOException, PersonException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); when(birthdaysClient.findFamousNamesOfAge(16)).thenReturn(Arrays.asList("JoeFamous Sixteen", "Another Person")); when(birthdaysClient.findFamousNamesBornOn(1, 2)).thenReturn(Arrays.asList("Jan TwoKnown")); Set<String> thoseInCommon = person.getThoseInCommon(); assertAll( setContains(thoseInCommon, "Another Person"), setContains(thoseInCommon, "Jan TwoKnown"), setContains(thoseInCommon, "JoeFamous Sixteen"), ()-> assertEquals(3, thoseInCommon.size()) ); } private <T> Executable setContains(Set<T> set, T expected) { return () -> assertTrue(set.contains(expected), "Should contain " + expected); } // ... }

Mantenha o código de teste limpo

Embora muitas vezes ignorado, é igualmente importante manter o código de teste livre de duplicação purulenta. Código limpo e princípios como “não se repita” são muito importantes para manter uma base de código de alta qualidade, produção e código de teste. Observe que o PersonTest.java mais recente tem alguma duplicação agora que temos vários métodos de teste.

Para corrigir isso, podemos fazer algumas coisas:

  • Extraia o objeto IOException em um campo final privado.

  • Extraia a criação do objeto Person em seu próprio método ( createJoeSixteenJan2() , neste caso), já que a maioria dos objetos Person está sendo criada com os mesmos parâmetros.

  • Crie um assertCauseAndMessage() para os vários testes que verificam PersonExceptions lançados.

Os resultados do código limpo podem ser vistos nesta versão do arquivo PersonTest.java.

Teste mais do que o caminho feliz

O que devemos fazer quando um objeto Person tem uma data de nascimento posterior à data atual? Defeitos em aplicativos geralmente são devidos a entradas inesperadas ou falta de previsão em casos de canto, borda ou limite. É importante tentar antecipar essas situações da melhor maneira possível, e os testes de unidade geralmente são um local apropriado para isso. Ao construir nosso Person e PersonTest , incluímos alguns testes para exceções esperadas, mas não foi de forma alguma completo. Por exemplo, usamos LocalDate que não representa ou armazena dados de fuso horário. Nossas chamadas para LocalDate.now() , no entanto, retornam um LocalDate com base no fuso horário padrão do sistema, que pode ser um dia anterior ou posterior ao do usuário de um sistema. Esses fatores devem ser considerados com testes e comportamentos apropriados implementados.

Os limites também devem ser testados. Considere um objeto Person com um método getDaysUntilBirthday() . O teste deve incluir se o aniversário da pessoa já passou ou não no ano atual, se o aniversário da pessoa é hoje e como um ano bissexto afeta o número de dias. Esses cenários podem ser cobertos verificando um dia antes do aniversário da pessoa, o dia e um dia após o aniversário da pessoa, onde o próximo ano é um ano bissexto. Aqui está o código de teste pertinente:

PersonTest.java
 // ... class PersonTest { private final Supplier<LocalDate> currentDateSupplier = ()-> LocalDate.parse("2015-05-02"); private final LocalDate ageJustOver5 = LocalDate.parse("2010-05-01"); private final LocalDate ageExactly5 = LocalDate.parse("2010-05-02"); private final LocalDate ageAlmost5 = LocalDate.parse("2010-05-03"); // ... @Test void testGetDaysUntilBirthday() { assertAll( createPersonAndAssertValue(ageAlmost5, 1, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageExactly5, 0, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageJustOver5, 365, Person::getDaysUntilBirthday) ); } private Executable createPersonAndAssertValue(LocalDate dateOfBirth, long expectedValue, Function<Person, Long> personLongFunction) { Person person = new Person("Given", "Sur", dateOfBirth, currentDateSupplier); long actualValue = personLongFunction.apply(person); return () -> assertEquals(expectedValue, actualValue); } }

Testes de integração

Nós nos concentramos principalmente em testes de unidade, mas o JUnit também pode ser usado para testes de integração, aceitação, funcionais e de sistema. Esses testes geralmente exigem mais código de configuração, por exemplo, iniciar servidores, carregar bancos de dados com dados conhecidos etc. Embora muitas vezes possamos executar milhares de testes de unidade em segundos, grandes conjuntos de testes de integrações podem levar minutos ou até horas para serem executados. Testes de integração geralmente não devem ser usados ​​para tentar cobrir cada permutação ou caminho através do código; testes unitários são mais apropriados para isso.

A criação de testes para aplicativos da Web que direcionam os navegadores da Web para preencher formulários, clicar em botões, aguardar o carregamento do conteúdo etc. e o artigo de Martin Fowler sobre Objetos de Página).

JUnit é eficaz para testar APIs RESTful com o uso de um cliente HTTP como Apache HTTP Client ou Spring Rest Template (HowToDoInJava.com fornece um bom exemplo).

No nosso caso com o objeto Person , um teste de integração poderia envolver o uso do BirthdaysClient real em vez de um mock, com uma configuração especificando a URL base do serviço People Birthdays. Um teste de integração usaria uma instância de teste desse serviço, verificaria se os aniversários foram publicados nele e criaria pessoas famosas no serviço que seriam retornadas.

Outros recursos do JUnit

JUnit tem muitos recursos adicionais que ainda não exploramos nos exemplos. Vamos descrever alguns e fornecer referências para outros.

Dispositivos de teste

Deve-se notar que o JUnit cria uma nova instância da classe de teste para executar cada método @Test . JUnit também fornece ganchos de anotação para executar métodos específicos antes ou depois de todos ou cada um dos métodos @Test . Esses ganchos são frequentemente usados ​​para configurar ou limpar banco de dados ou objetos simulados e diferem entre JUnit 4 e 5.

JUnit 4 JUnit 5 Para um método estático?
@BeforeClass @BeforeAll sim
@AfterClass @AfterAll sim
@Before @BeforeEach Não
@After @AfterEach Não

Em nosso exemplo PersonTest , optamos por configurar o objeto simulado BirthdaysClient nos próprios métodos @Test , mas às vezes estruturas simuladas mais complexas precisam ser construídas envolvendo vários objetos. @BeforeEach (no JUnit 5) e @Before (no JUnit 4) geralmente são apropriados para isso.

As anotações @After* são mais comuns com testes de integração do que com testes de unidade, pois a coleta de lixo da JVM manipula a maioria dos objetos criados para testes de unidade. As anotações @BeforeClass e @BeforeAll são mais comumente usadas para testes de integração que precisam executar ações dispendiosas de configuração e desmontagem uma vez, em vez de para cada método de teste.

Para JUnit 4, consulte o guia de acessórios de teste (os conceitos gerais ainda se aplicam ao JUnit 5).

Conjuntos de teste

Às vezes, você deseja executar vários testes relacionados, mas não todos os testes. Nesse caso, os agrupamentos de testes podem ser compostos em conjuntos de testes. Para saber como fazer isso no JUnit 5, confira o artigo do JUnit 5 do HowToProgram.xyz e a documentação da equipe do JUnit para o JUnit 4.

@Nested e @DisplayName do JUnit 5

O JUnit 5 adiciona a capacidade de usar classes internas aninhadas não estáticas para mostrar melhor o relacionamento entre os testes. Isso deve ser muito familiar para aqueles que trabalharam com descrições aninhadas em estruturas de teste como Jasmine para JavaScript. As classes internas são anotadas com @Nested para usar isso.

A anotação @DisplayName também é nova no JUnit 5, permitindo que você descreva o teste para relatório em formato de string, a ser mostrado além do identificador do método de teste.

Embora @Nested e @DisplayName possam ser usados ​​independentemente um do outro, juntos eles podem fornecer resultados de teste mais claros que descrevem o comportamento do sistema.

Combinadores de Hamcrest

O framework Hamcrest, embora não faça parte da base de código JUnit, oferece uma alternativa ao uso de métodos assert tradicionais em testes, permitindo um código de teste mais expressivo e legível. Veja a verificação a seguir usando um assertEquals tradicional e um assertThat Hamcrest:

 //Traditional assert assertEquals("Hayden, Josh", displayName); //Hamcrest assert assertThat(displayName, equalTo("Hayden, Josh"));

Hamcrest pode ser usado com JUnit 4 e 5. O tutorial do Vogella.com sobre Hamcrest é bastante abrangente.

Recursos adicionais

  • O artigo Testes unitários, como escrever código testável e por que é importante abrange exemplos mais específicos de como escrever código limpo e testável.

  • Construa com confiança: um guia para testes JUnit examina diferentes abordagens para testes de unidade e integração e por que é melhor escolher uma e mantê-la

  • O JUnit 4 Wiki e o JUnit 5 User Guide são sempre um excelente ponto de referência.

  • A documentação do Mockito fornece informações sobre funcionalidades e exemplos adicionais.

JUnit é o caminho para a automação

Exploramos muitos aspectos do teste no mundo Java com JUnit. Examinamos testes de unidade e integração usando o framework JUnit para bases de código Java, integrando JUnit em ambientes de desenvolvimento e construção, como usar mocks e stubs com fornecedores e Mockito, convenções comuns e melhores práticas de código, o que testar e alguns dos outros ótimos recursos do JUnit.

Agora é a vez do leitor crescer em aplicar, manter e colher habilmente os benefícios de testes automatizados usando a estrutura JUnit.