CodeGym /Blogue Java /Random-PT /Tudo sobre testes unitários: técnicas, conceitos, prática...
John Squirrels
Nível 41
San Francisco

Tudo sobre testes unitários: técnicas, conceitos, prática

Publicado no grupo Random-PT
Hoje você não encontrará um aplicativo que não esteja repleto de testes, portanto, este tópico será mais relevante do que nunca para desenvolvedores novatos: você não pode ter sucesso sem testes. Vamos considerar quais tipos de teste são usados ​​em princípio e, em seguida, estudaremos em detalhes tudo o que há para saber sobre testes de unidade. Tudo sobre testes unitários: técnicas, conceitos, prática - 1

Tipos de teste

O que é um teste? De acordo com a Wikipedia: "O teste de software envolve a execução de um componente de software ou componente do sistema para avaliar uma ou mais propriedades de interesse." Em outras palavras, é uma verificação da correção do nosso sistema em determinadas situações. Bem, vamos ver que tipos de testes existem em geral:
  • Teste de unidade — Testes cujo objetivo é verificar cada módulo do sistema separadamente. Esses testes devem ser aplicados às menores partes atômicas do sistema, por exemplo, módulos.
  • Teste do sistema — Teste de alto nível para verificar a operação de uma parte maior do aplicativo ou do sistema como um todo.
  • Teste de regressão — Teste usado para verificar se novos recursos ou correções de bugs afetam a funcionalidade existente do aplicativo ou introduzem bugs antigos.
  • Teste funcional — Verificar se uma parte do aplicativo atende aos requisitos estabelecidos nas especificações, histórias de usuários, etc.

    Tipos de testes funcionais:

    • Teste de caixa branca — Verificar se uma parte do aplicativo atende aos requisitos conhecendo a implementação interna do sistema;
    • Teste de caixa preta — Verifica se uma parte do aplicativo atende aos requisitos sem conhecer a implementação interna do sistema.

  • Teste de desempenho — Testes escritos para determinar como o sistema ou parte do sistema funciona sob uma determinada carga.
  • Teste de carga — Testes projetados para verificar a estabilidade do sistema sob cargas padrão e para encontrar a carga máxima na qual o aplicativo ainda funciona corretamente.
  • Teste de estresse — Teste projetado para verificar o desempenho do aplicativo sob cargas fora do padrão e para determinar a carga máxima antes da falha do sistema.
  • Teste de segurança — Testes usados ​​para verificar a segurança do sistema (de hackers, vírus, acesso não autorizado a dados confidenciais e outros ataques deliciosos).
  • Teste de localização — Testes de localização do aplicativo.
  • Testes de usabilidade — Testes destinados a verificar usabilidade, compreensibilidade, atratividade e capacidade de aprendizado.
Tudo isso soa bem, mas como funciona na prática? Simples! Usamos a pirâmide de testes de Mike Cohn: Tudo sobre testes unitários: técnicas, conceitos, prática - 2Esta é uma versão simplificada da pirâmide: ela agora está dividida em partes ainda menores. Mas hoje não vamos ficar muito sofisticados. Vamos considerar a versão mais simples.
  1. Unidade — Esta seção refere-se aos testes de unidade, que são aplicados em diferentes camadas do aplicativo. Eles testam a menor unidade divisível da lógica do aplicativo. Por exemplo, classes, mas na maioria das vezes métodos. Esses testes geralmente tentam o máximo possível isolar o que é testado de qualquer lógica externa. Ou seja, eles tentam criar a ilusão de que o restante do aplicativo está sendo executado conforme o esperado.

    Sempre deve haver muitos desses testes (mais do que qualquer outro tipo), pois eles testam peças pequenas e são muito leves, não consumindo muitos recursos (ou seja, RAM e tempo).

  2. Integração — Esta seção refere-se ao teste de integração. Este teste verifica partes maiores do sistema. Ou seja, combina várias peças de lógica (vários métodos ou classes) ou verifica a exatidão da interação com um componente externo. Esses testes geralmente são menores que os testes de unidade porque são mais pesados.

    Um exemplo de teste de integração pode ser conectar-se a um banco de dados e verificar a exatidão da operação dos métodos para trabalhar com ele.

  3. UI — Esta seção refere-se a testes que verificam a operação da interface do usuário. Eles envolvem a lógica em todos os níveis do aplicativo, por isso também são chamados de testes de ponta a ponta. Via de regra, são bem menos numerosos, pois são os mais incômodos e devem verificar os caminhos mais necessários (usados).

    Na figura acima, vemos que as diferentes partes do triângulo variam em tamanho: aproximadamente as mesmas proporções existem no número de diferentes tipos de testes no trabalho real.

    Hoje vamos dar uma olhada mais de perto nos testes mais comuns, testes de unidade, já que todos os desenvolvedores Java que se preze devem ser capazes de usá-los em um nível básico.

Conceitos-chave em testes de unidade

A cobertura de teste (cobertura de código) é uma das principais medidas de quão bem um aplicativo é testado. Esta é a porcentagem do código coberta pelos testes (0-100%). Na prática, muitos buscam esse percentual como meta. Isso é algo que eu discordo, pois significa que testes passam a ser aplicados onde não são necessários. Por exemplo, suponha que tenhamos operações CRUD (criar/obter/atualizar/excluir) padrão em nosso serviço sem lógica adicional. Esses métodos são puramente intermediários que delegam trabalho para a camada que trabalha com o repositório. Nesta situação, não temos nada para testar, exceto talvez se o método dado chama um método DAO, mas isso é uma piada. Ferramentas adicionais geralmente são usadas para avaliar a cobertura do teste: JaCoCo, Cobertura, Clover, Emma, ​​etc. Para um estudo mais detalhado deste tópico, TDD significa desenvolvimento orientado a testes. Nesta abordagem, antes de fazer qualquer outra coisa, você escreve um teste que verificará o código específico. Isso acaba sendo um teste de caixa preta: sabemos qual é a entrada e qual deve ser a saída. Isso torna possível evitar a duplicação de código. O desenvolvimento orientado a testes começa com o design e desenvolvimento de testes para cada funcionalidade do seu aplicativo. Na abordagem TDD, primeiro criamos um teste que define e testa o comportamento do código. O principal objetivo do TDD é tornar seu código mais compreensível, simples e livre de erros. Tudo sobre testes unitários: técnicas, conceitos, prática - 3A abordagem consiste no seguinte:
  • Nós escrevemos nosso teste.
  • Fazemos o teste. Sem surpresa, ele falha, pois ainda não implementamos a lógica necessária.
  • Adicione o código que faz com que o teste seja aprovado (executamos o teste novamente).
  • Refatoramos o código.
O TDD é baseado em testes de unidade, pois eles são os menores blocos de construção na pirâmide de automação de teste. Com os testes de unidade, podemos testar a lógica de negócios de qualquer classe. BDD significa desenvolvimento orientado por comportamento. Essa abordagem é baseada em TDD. Mais especificamente, ele usa exemplos de linguagem simples que explicam o comportamento do sistema para todos os envolvidos no desenvolvimento. Não vamos nos aprofundar neste termo, pois afeta principalmente testadores e analistas de negócios. Um caso de teste é um cenário que descreve as etapas, condições específicas e parâmetros necessários para verificar o código em teste. Um acessório de teste é um código que configura o ambiente de teste para ter o estado necessário para que o método em teste seja executado com sucesso. É um conjunto predefinido de objetos e seu comportamento sob condições especificadas.

Etapas de teste

Um teste consiste em três etapas:
  • Especifique os dados de teste (acessórios).
  • Exercite o código em teste (chame o método testado).
  • Verifique os resultados e compare com os resultados esperados.
Tudo sobre testes unitários: técnicas, conceitos, prática - 4Para garantir a modularidade do teste, você precisa se isolar de outras camadas do aplicativo. Isso pode ser feito usando stubs, mocks e espiões. Mocks são objetos que podem ser personalizados (por exemplo, adaptados para cada teste). Eles nos permitem especificar o que esperamos das chamadas de método, ou seja, as respostas esperadas. Usamos objetos fictícios para verificar se obtemos o que esperamos. Os stubs fornecem uma resposta codificada para chamadas durante o teste. Eles também podem armazenar informações sobre a chamada (por exemplo, parâmetros ou o número de chamadas). Às vezes, eles são chamados de espiões. Às vezes as pessoas confundem os termos stub e mock: a diferença é que um stub não verifica nada — apenas simula um determinado estado. Um mock é um objeto que tem expectativas. Por exemplo, que um determinado método deve ser chamado um certo número de vezes. Em outras palavras,

ambientes de teste

Então, agora vamos ao que interessa. Existem vários ambientes de teste (frameworks) disponíveis para Java. Os mais populares são JUnit e TestNG. Para nossa revisão aqui, usamos: Tudo sobre testes unitários: técnicas, conceitos, prática - 5Um teste JUnit é um método em uma classe que é usado apenas para teste. A classe geralmente tem o mesmo nome da classe que ela testa, com "Test" anexado ao final. Por exemplo, CarService -> CarServiceTest. O sistema de compilação do Maven inclui automaticamente essas classes no escopo do teste. Na verdade, essa classe é chamada de classe de teste. Vamos examinar brevemente as anotações básicas:

  • @Test indica que o método é um teste (basicamente, um método marcado com esta anotação é um teste de unidade).
  • @Before significa um método que será executado antes de cada teste. Por exemplo, para preencher uma classe com dados de teste, ler dados de entrada, etc.
  • @After é usado para marcar um método que será chamado após cada teste (por exemplo, para limpar dados ou restaurar valores padrão).
  • @BeforeClass é colocado acima de um método, análogo a @Before. Mas tal método é chamado apenas uma vez antes de todos os testes para a classe dada e, portanto, deve ser estático. Ele é usado para executar operações com uso intensivo de recursos, como ativar um banco de dados de teste.
  • @AfterClass é o oposto de @BeforeClass: é executado uma vez para a classe dada, mas somente após todos os testes. É usado, por exemplo, para limpar recursos persistentes ou desconectar de um banco de dados.
  • @Ignore indica que um método está desabilitado e será ignorado durante a execução geral do teste. Isso é usado em várias situações, por exemplo, se o método base foi alterado e o teste ainda não foi reformulado para acomodar as alterações. Nesses casos, também é desejável adicionar uma descrição, ou seja, @Ignore("Alguma descrição").
  • @Test(expected = Exception.class) é usado para testes negativos. São testes que verificam como o método se comporta em caso de erro, ou seja, o teste espera que o método lance algum tipo de exceção. Tal método é indicado pela anotação @Test, mas com uma indicação de qual erro capturar.
  • @Test(timeout = 100) verifica se o método é executado em no máximo 100 milissegundos.
  • @Mock é usado acima de um campo para atribuir um objeto fictício (não é uma anotação JUnit, mas vem de Mockito). Conforme necessário, definimos o comportamento do mock para uma situação específica diretamente no método de teste.
  • @RunWith(MockitoJUnitRunner.class) é colocado acima de uma classe. Essa anotação diz ao JUnit para invocar os testes na classe. Existem vários executores, incluindo estes: MockitoJUnitRunner, JUnitPlatform e SpringRunner. No JUnit 5, a anotação @RunWith foi substituída pela anotação mais poderosa @ExtendWith.
Vamos dar uma olhada em alguns métodos usados ​​para comparar resultados:

  • assertEquals(Objeto esperado, Objeto real) — verifica se os objetos passados ​​são iguais.
  • assertTrue(boolean flag) — verifica se o valor passado é verdadeiro.
  • assertFalse(boolean flag) — verifica se o valor passado é falso.
  • assertNull(Object object) — verifica se o objeto passado é nulo.
  • assertSame(Object firstObject, Object secondObject) — verifica se os valores passados ​​referem-se ao mesmo objeto.
  • assertThat(T t, Matcher correspondente) — Verifica se t satisfaz a condição especificada em matcher.
AssertJ também fornece um método de comparação útil: assertThat(firstObject).isEqualTo(secondObject) . Aqui mencionei os métodos básicos — os outros são variações dos anteriores.

Testando na prática

Agora vamos ver o material acima em um exemplo específico. Testaremos o método de atualização de um serviço. Não vamos considerar a camada DAO, pois estamos usando o padrão. Vamos adicionar um starter para os testes:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <version>2.2.2.RELEASE</version>
   <scope>test</scope>
</dependency>
E aqui temos a classe de serviço:

@Service
@RequiredArgsConstructor
public class RobotServiceImpl implements RobotService {
   private final RobotDAO robotDAO;

   @Override
   public Robot update(Long id, Robot robot) {
       Robot found = robotDAO.findById(id);
       return robotDAO.update(Robot.builder()
               .id(id)
               .name(robot.getName() != null ? robot.getName() : found.getName())
               .cpu(robot.getCpu() != null ? robot.getCpu() : found.getCpu())
               .producer(robot.getProducer() != null ? robot.getProducer() : found.getProducer())
               .build());
   }
}
Linha 8 — extrai o objeto atualizado do banco de dados. Linhas 9-14 — criam um objeto por meio do construtor. Se o objeto recebido tiver um campo, defina-o. Se não, vamos deixar o que está no banco de dados. Agora veja nosso teste:

@RunWith(MockitoJUnitRunner.class)
public class RobotServiceImplTest {
   @Mock
   private RobotDAO robotDAO;

   private RobotServiceImpl robotService;

   private static Robot testRobot;

   @BeforeClass
   public static void prepareTestData() {
       testRobot = Robot
               .builder()
               .id(123L)
               .name("testRobotMolly")
               .cpu("Intel Core i7-9700K")
               .producer("China")
               .build();
   }

   @Before
   public void init() {
       robotService = new RobotServiceImpl(robotDAO);
   }
Linha 1 — nosso corredor. Linha 4 — isolamos o serviço da camada DAO substituindo por mock. Linha 11 — definimos uma entidade de teste (aquela que usaremos como cobaia) para a classe. Linha 22 — configuramos o objeto de serviço, que é o que vamos testar.

@Test
public void updateTest() {
   when(robotDAO.findById(any(Long.class))).thenReturn(testRobot);
   when(robotDAO.update(any(Robot.class))).then(returnsFirstArg());
   Robot robotForUpdate = Robot
           .builder()
           .name("Vally")
           .cpu("AMD Ryzen 7 2700X")
           .build();

   Robot resultRobot = robotService.update(123L, robotForUpdate);

   assertNotNull(resultRobot);
   assertSame(resultRobot.getId(),testRobot.getId());
   assertThat(resultRobot.getName()).isEqualTo(robotForUpdate.getName());
   assertTrue(resultRobot.getCpu().equals(robotForUpdate.getCpu()));
   assertEquals(resultRobot.getProducer(),testRobot.getProducer());
}
Aqui vemos que o teste tem três divisões claras: Linhas 3-9 — especificando acessórios. Linha 11 — executando o código em teste. Linhas 13-17 — verificando os resultados. Mais detalhadamente: Linhas 3-4 — defina o comportamento para a simulação DAO. Linha 5 — define a instância que iremos atualizar em cima do nosso padrão. Linha 11 — use o método e pegue a instância resultante. Linha 13 — verifique se não é nulo. Linha 14 — compara o ID do resultado e os argumentos do método fornecidos. Linha 15 — verifica se o nome foi atualizado. Linha 16 — veja o resultado da CPU. Linha 17 — não especificamos este campo na instância, então ele deve permanecer o mesmo. Verificamos essa condição aqui. Vamos executá-lo:Tudo sobre testes unitários: técnicas, conceitos, prática - 6O teste é verde! Podemos respirar aliviados :) Em resumo, testar melhora a qualidade do código e torna o processo de desenvolvimento mais flexível e confiável. Imagine quanto esforço é necessário para redesenhar software envolvendo centenas de arquivos de classe. Quando temos testes de unidade escritos para todas essas classes, podemos refatorar com confiança. E o mais importante, nos ajuda a encontrar bugs facilmente durante o desenvolvimento. Rapazes e moças, isso é tudo que tenho hoje. Dê um like e deixe seu comentário :)
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION