JavaRush /Blogue Java /Random-PT /Teste de unidade Java: técnicas, conceitos, prática

Teste de unidade Java: técnicas, conceitos, prática

Publicado no grupo Random-PT
Hoje você dificilmente encontrará um aplicativo que não seja coberto por testes, então este tópico será mais relevante do que nunca para desenvolvedores novatos: sem testes você não chega a lugar nenhum. Como propaganda, sugiro que você dê uma olhada em meus artigos anteriores. Alguns deles abrangem testes (e mesmo assim os artigos serão muito úteis):
  1. Teste de integração de um banco de dados usando MariaDB para substituir MySql
  2. Implementação de aplicação multilíngue
  3. Salvando arquivos no aplicativo e dados sobre eles no banco de dados
Vamos considerar quais tipos de testes são usados ​​​​em princípio e depois estudaremos detalhadamente tudo o que você precisa saber sobre testes unitários.

Tipos de testes

O que é um teste? Como diz o Wiki: “ Um teste ou teste é uma forma de estudar os processos subjacentes de um sistema, colocando o sistema em diferentes situações e rastreando mudanças observáveis ​​nele.” Ou seja, trata-se de um teste do correto funcionamento do nosso sistema em determinadas situações. Tudo sobre testes unitários: métodos, conceitos, prática - 2Bem, vamos ver que tipos de testes existem:
  1. Testes unitários são testes cuja tarefa é testar cada módulo do sistema individualmente. É desejável que estas sejam peças minimamente divisíveis do sistema, por exemplo, módulos.

  2. O teste de sistema é um teste de alto nível para testar a operação de uma parte maior de um aplicativo ou do sistema como um todo.

  3. O teste de regressão é um teste usado para verificar se novos recursos ou correções de bugs afetam a funcionalidade existente do aplicativo e se bugs antigos reaparecem.

  4. O teste funcional verifica a conformidade de parte do aplicativo com os requisitos declarados nas especificações, histórias de usuários, etc.

    Tipos de testes funcionais:

    • teste “caixa branca” para conformidade de parte da aplicação com os requisitos com conhecimento da implementação interna do sistema;
    • Teste “caixa preta” para conformidade de parte da aplicação com os requisitos sem conhecimento da implementação interna do sistema.
  5. O teste de desempenho é um tipo de teste escrito para determinar a velocidade na qual um sistema ou parte dele é executado sob uma determinada carga.
  6. Teste de carga - testes projetados para verificar a estabilidade do sistema sob cargas padrão e encontrar o pico máximo possível no qual o aplicativo funciona corretamente.
  7. O teste de estresse é um tipo de teste projetado para verificar a funcionalidade de um aplicativo sob cargas fora do padrão e para determinar o pico máximo possível no qual o sistema não travará.
  8. Testes de segurança - testes usados ​​para verificar a segurança de um sistema (contra ataques de hackers, vírus, acesso não autorizado a dados confidenciais e outras alegrias da vida).
  9. O teste de localização é um teste de localização para um aplicativo.
  10. O teste de usabilidade é um tipo de teste que visa verificar a usabilidade, compreensibilidade, atratividade e capacidade de aprendizagem para os usuários.
  11. Tudo isso parece bom, mas como funciona na prática? É simples: é usada a pirâmide de testes de Mike Cohn: Tudo sobre testes unitários: métodos, conceitos, prática - 4Esta é uma versão simplificada da pirâmide: agora está dividida em partes menores. Mas hoje não vamos perverter e considerar a opção mais simples.
    1. Unidade - testes unitários usados ​​em várias camadas da aplicação, testando a menor lógica divisível da aplicação: por exemplo, uma classe, mas na maioria das vezes um método. Esses testes geralmente tentam isolar o máximo possível da lógica externa, ou seja, criar a ilusão de que o restante da aplicação está funcionando em modo padrão.

      Deve haver sempre muitos desses testes (mais do que outros tipos), pois eles testam peças pequenas e são muito leves, não consumindo muitos recursos (por recursos quero dizer RAM e tempo).

    2. Integração - teste de integração. Ele verifica partes maiores do sistema, ou seja, é uma combinação de várias partes da lógica (vários métodos ou classes), ou a correção de trabalhar com um componente externo. Geralmente há menos desses testes do que testes de unidade, pois são mais pesados.

      Como exemplo de testes de integração, você pode considerar conectar-se a um banco de dados e verificar a execução correta dos métodos que trabalham com ele .

    3. UI - testes que verificam o funcionamento da interface do usuário. Eles afetam a lógica em todos os níveis da aplicação, por isso também são chamados de ponta a ponta. Via de regra, são muito menos, portanto são os mais pesados ​​e devem verificar os caminhos mais necessários (usados).

      Na figura acima vemos a proporção das áreas das diferentes partes do triângulo: aproximadamente a mesma proporção é mantida no número desses testes no trabalho real.

      Hoje daremos uma olhada mais de perto nos testes mais usados ​​- testes unitários, já que todos os desenvolvedores Java que se prezem devem ser capazes de usá-los em um nível básico.

    Conceitos-chave de testes unitários

    A cobertura de teste (cobertura de código) é uma das principais avaliações da qualidade dos testes de aplicações. Esta é a porcentagem de código que foi coberto pelos testes (0-100%). Na prática, muitas pessoas correm atrás desse percentual, o que não concordo, pois passam a adicionar testes onde não são necessários. Por exemplo, nosso serviço possui operações CRUD padrão (criar/obter/atualizar/excluir) sem lógica adicional. Esses métodos são puramente intermediários que delegam trabalho à camada que trabalha com o repositório. Nesta situação, não temos nada para testar: talvez se este método chama um método do Tao, mas isto não é sério. Para avaliar a cobertura do teste, geralmente são utilizadas ferramentas adicionais: JaCoCo, Cobertura, Clover, Emma, ​​​​etc. Para um estudo mais detalhado deste assunto, guarde alguns artigos adequados: TDD (desenvolvimento orientado a testes) - desenvolvimento orientado a testes. Nesta abordagem, em primeiro lugar, é escrito um teste que irá verificar um código específico. Acontece que é um teste de caixa preta: sabemos o que está na entrada e o que deve acontecer na saída. Isso evita duplicação de código. O desenvolvimento orientado a testes começa com o projeto e desenvolvimento de testes para cada pequena funcionalidade do aplicativo. Na abordagem TDD, primeiro é desenvolvido um teste que define e verifica o que o código fará. O principal objetivo do TDD é tornar o código mais claro, simples e livre de erros. Tudo sobre testes unitários: métodos, conceitos, prática - 6A abordagem consiste nos seguintes componentes:
    1. Estamos escrevendo nosso teste.
    2. Executamos o teste, tenha passado ou não (vemos que está tudo vermelho - não se desespere: é assim que deve ser).
    3. Adicionamos o código que deve satisfazer este teste (executamos o teste).
    4. Refatoramos o código.
    Com base no fato de que os testes unitários são os menores elementos da pirâmide de automação de testes, o TDD é baseado neles. Com a ajuda de testes unitários podemos testar a lógica de negócio de qualquer classe. BDD (Behavior-driven development) - desenvolvimento através do comportamento. Esta abordagem é baseada em TDD. Mais especificamente, utiliza exemplos escritos em linguagem clara (geralmente em inglês) que ilustram o comportamento do sistema para todos os envolvidos no desenvolvimento. Não iremos nos aprofundar neste termo, pois ele afeta principalmente testadores e analistas de negócios. Caso de Teste - um script que descreve as etapas, condições específicas e parâmetros necessários para verificar a implementação do código em teste. Fixture é um estado do ambiente de teste necessário para a execução bem-sucedida do método em teste. Este é um conjunto predeterminado de objetos e seu comportamento nas condições utilizadas.

    Estágios de teste

    A prova consiste em três etapas:
    1. Especificando os dados a serem testados (acessórios).
    2. Usando o código em teste (chamando o método em teste).
    3. Verificando os resultados e comparando-os com os esperados.
    Tudo sobre testes unitários: métodos, conceitos, prática - 7Para garantir a modularidade do teste, você precisa estar isolado de outras camadas do aplicativo. Isso pode ser feito usando stubs, mocks e espiões. Mocks são objetos customizáveis ​​(por exemplo, específicos para cada teste) e permitem definir expectativas para chamadas de métodos na forma de respostas que planejamos receber. As verificações de expectativas são realizadas por meio de chamadas para objetos Mock. Stubs - fornecem uma resposta conectada às chamadas durante o teste. Eles também podem armazenar informações sobre a chamada (por exemplo, parâmetros ou o número dessas chamadas). Às vezes, eles são chamados por seu próprio termo - espião ( espião ). Às vezes esses termos stubs e mock são confundidos: a diferença é que um stub não verifica nada, apenas simula um determinado estado. Uma simulação é um objeto que tem expectativas. Por exemplo, que um determinado método de classe deve ser chamado um certo número de vezes. Em outras palavras, seu teste nunca será interrompido devido a um stub, mas poderá ser interrompido devido a uma simulação.

    Ambientes de teste

    Então agora vamos ao que interessa. Existem vários ambientes de teste (frameworks) disponíveis para Java. Os mais populares deles são JUnit e TestNG. Para nossa revisão, usamos: Tudo sobre testes unitários: métodos, conceitos, prática - 8Um teste JUnit é um método contido em uma classe usado apenas para teste. Uma classe normalmente recebe o mesmo nome da classe que está testando, com +Test no final. Por exemplo, CarService → CarServiceTest. O sistema de compilação Maven inclui automaticamente essas classes na área de teste. Na verdade, essa classe é chamada de classe de teste. Vamos examinar um pouco as anotações básicas: @Test - definição deste método como método de teste (na verdade, o método marcado com esta anotação é um teste unitário). @Before – marca o método que será executado antes de cada teste. Por exemplo, preenchimento de dados de teste de classe, leitura de dados de entrada, etc. @After - colocado acima do método que será chamado após cada teste (limpeza de dados, restauração de valores padrão). @BeforeClass - colocado acima do método - análogo a @Before. Mas este método é chamado apenas uma vez antes de todos os testes de uma determinada classe e, portanto, deve ser estático. Ele é usado para realizar operações mais pesadas, como levantar um banco de dados de teste. @AfterClass é o oposto de @BeforeClass: executado uma vez para uma determinada classe, mas executado após todos os testes. Usado, por exemplo, para limpar recursos persistentes ou desconectar-se do banco de dados. @Ignore - observa que o método abaixo está desabilitado e será ignorado durante a execução geral dos testes. É utilizado em diversos casos, por exemplo, se o método base foi alterado e não houve tempo para refazer o teste. Nesses casos, também é aconselhável adicionar uma descrição - @Ignore("Alguma descrição"). @Test (expected = Exception.class) - usado para testes negativos. São testes que verificam como um método se comporta em caso de erro, ou seja, o teste espera que o método lance alguma exceção. Tal método é indicado pela anotação @Test, mas com um erro a ser detectado. @Test(timeout=100) - verifica se o método é executado em no máximo 100 milissegundos. @Mock - uma classe é usada sobre um campo para definir um determinado objeto como um mock (isso não é da biblioteca Junit, mas do Mockito), e se precisarmos, definiremos o comportamento do mock em uma situação específica , diretamente no método de teste. @RunWith(MockitoJUnitRunner.class) - o método é colocado acima da classe. Este é o botão para executar testes nele. Os corredores podem ser diferentes: por exemplo, existem os seguintes: MockitoJUnitRunner, JUnitPlatform, SpringRunner, etc.). No JUnit 5, a anotação @RunWith foi substituída pela anotação @ExtendWith mais poderosa. Vamos dar uma olhada em alguns métodos para comparar resultados:
    • assertEquals(Object expecteds, Object actuals)— verifica se os objetos transmitidos são iguais.
    • assertTrue(boolean flag)— verifica se o valor passado retorna verdadeiro.
    • assertFalse(boolean flag)— verifica se o valor passado retorna falso.
    • assertNull(Object object)– verifica se o objeto é nulo.
    • assertSame(Object firstObject, Object secondObject)— verifica se os valores passados ​​referem-se ao mesmo objeto.
    • assertThat(T t, Matcher<T> matcher)— verifica se t satisfaz a condição especificada no matcher.
    Há também um formulário de comparação útil do assertj - assertThat(firstObject).isEqualTo(secondObject) aqui falei sobre os métodos básicos, já que o restante são variações diferentes dos métodos acima.

    Prática de teste

    Agora vamos examinar o material acima usando um exemplo específico. Testaremos o método para o serviço - atualização. Não consideraremos a camada dao, pois é o nosso padrão. Vamos adicionar um starter para testes:
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-test</artifactId>
       <version>2.2.2.RELEASE</version>
       <scope>test</scope>
    </dependency>
    Então, 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());
       }
    }
    8 - extraia o objeto atualizado do banco de dados 9-14 - crie o objeto através do construtor, se o objeto de entrada tiver um campo - configure-o, se não - deixe o que está no banco de dados E 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);
       }
    1 — nosso Runner 4 — isolar o serviço da camada dao substituindo um mock 11 — definir uma entidade de teste para a classe (aquela que usaremos como hamster de teste) 22 — definir um objeto de serviço que testaremos
    @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 uma divisão clara do teste em três partes: 3-9 - configurando os fixtures 11 - executando a parte testada 13-17 - verificando os resultados Mais detalhes: 3-4 - configurando o comportamento do moka dao 5 - configurando a instância que atualizaremos sobre nosso padrão 11 - use o método e pegue a instância resultante 13 - verifique se não é zero 14 - verifique o ID do resultado e os argumentos do método especificado 15 - verifique se o nome foi atualizado 16 - veja o resultado pela cpu 17 - como não definimos isso no campo update instance, deve permanecer igual, vamos verificar. Tudo sobre testes unitários: métodos, conceitos, prática - 9Vamos lançar: Tudo sobre testes unitários: técnicas, conceitos, prática – 10O teste é verde, pode expirar)) Então, vamos resumir: o teste melhora a qualidade do código e torna o processo de desenvolvimento mais flexível e confiável. Imagine quanto esforço teríamos que gastar ao redesenhar software com centenas de arquivos de classe. Assim que tivermos testes unitários escritos para todas essas classes, poderemos refatorar com confiança. E o mais importante, nos ajuda a encontrar erros facilmente durante o desenvolvimento. Pessoal, isso é tudo para mim hoje: dêem curtidas, escrevam comentários))) Tudo sobre testes unitários: métodos, conceitos, prática - 11
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION