Como finalmente começar a escrever testes e não se arrepender



Chegando a um novo projeto, encontro regularmente uma das seguintes situações:

  1. Não há testes.
  2. Existem poucos testes, eles raramente são escritos e não são executados continuamente.
  3. Os testes estão presentes e incluídos no CI (integração contínua), mas causam mais danos do que benefícios.

Infelizmente, é o último cenário que geralmente leva a sérias tentativas de iniciar a implementação de testes na ausência de habilidades apropriadas.

O que pode ser feito para mudar a situação atual? A ideia de usar testes não é nova. Ao mesmo tempo, a maioria dos tutoriais se assemelha à famosa figura sobre como desenhar uma coruja: conecte o JUnit, escreva o primeiro teste, use o primeiro simulador - e pronto! Esses artigos não respondem a perguntas sobre quais testes precisam ser escritos, em que vale a pena prestar atenção e como conviver com tudo isso. A partir daqui, a ideia deste artigo nasceu. Tentei resumir brevemente minha experiência na implementação de testes em diferentes projetos, a fim de facilitar esse caminho para todos.


Existem artigos introdutórios mais do que suficientes sobre esse tópico, portanto não nos repetiremos e tentaremos ir do outro lado. Na primeira parte, desbancaremos o mito de que o teste acarreta custos exclusivamente adicionais. Será mostrado como a criação de testes de qualidade, por sua vez, pode acelerar o processo de desenvolvimento. Então, no exemplo de um projeto pequeno, serão considerados os princípios e regras básicos que devem ser seguidos para obter esse benefício. Finalmente, na seção final, recomendações específicas de implementação serão fornecidas: como evitar problemas típicos quando os testes começam, pelo contrário, diminuem significativamente o desenvolvimento.

Como minha principal especialização é o Java back-end, a seguinte pilha de tecnologia será usada nos exemplos: Java, JUnit, H2, Mockito, Spring, Hibernate. Ao mesmo tempo, uma parte significativa do artigo é dedicada a problemas gerais de teste e as dicas são aplicáveis ​​a uma gama muito maior de tarefas.

No entanto, tenha cuidado! Os testes são muito viciantes: depois que você aprender a usá-los, não poderá mais viver sem eles.


Testes versus velocidade de desenvolvimento


As principais perguntas que surgem ao discutir a implementação de testes: quanto tempo levará para escrever testes e quais benefícios ele terá? Os testes, como qualquer outra tecnologia, exigirão esforços sérios para desenvolvimento e implementação, portanto, a princípio, nenhum benefício significativo deve ser esperado. Quanto aos custos de tempo, eles são altamente dependentes da equipe em particular. No entanto, menos de 20 a 30% dos custos adicionais da codificação não devem ser calculados exatamente. Menos simplesmente não é suficiente para alcançar pelo menos algum resultado. A expectativa de retornos instantâneos é frequentemente o principal motivo para restringir essa atividade antes mesmo que os testes se tornem úteis.

Mas de que tipo de eficiência estamos falando? Vamos soltar as letras das dificuldades de implementação e ver quais oportunidades específicas para economizar tempo nos testes se abrem.

Executando código em qualquer lugar


Se não houver testes no projeto, a única maneira de começar é levantar o aplicativo inteiro. É bom que demore cerca de 15 a 20 segundos, mas os casos de grandes projetos nos quais um lançamento completo pode demorar alguns minutos estão longe de ser raros. O que isso significa para os desenvolvedores? Uma parte significativa de seu tempo de trabalho serão essas breves sessões de espera, durante as quais é impossível continuar trabalhando na tarefa atual, mas ao mesmo tempo, há muito pouco tempo para mudar para outra coisa. Muitos já encontraram pelo menos uma vez esses projetos em que o código escrito em uma hora requer muitas horas de depuração devido a longas reinicializações entre as correções. Nos testes, você pode limitar-se a executar pequenas partes do aplicativo, o que reduzirá significativamente o tempo de espera e aumentará a produtividade do trabalho no código.

Além disso, a capacidade de executar código em qualquer lugar leva a uma depuração mais completa. Frequentemente, verificar até os principais casos de uso positivos por meio da interface do aplicativo requer muito esforço e tempo. A presença de testes torna possível realizar uma verificação detalhada de uma funcionalidade específica com muito mais facilidade e rapidez.

Outra vantagem é a capacidade de regular o tamanho da unidade testada. Dependendo da complexidade da lógica que está sendo testada, você pode restringir-se a um método, uma classe, um grupo de classes que implementa alguma funcionalidade, um serviço etc., até a automação do teste de todo o aplicativo. Essa flexibilidade permite descarregar testes de alto nível de várias partes devido ao fato de que eles serão testados em níveis mais baixos.

Relançando testes


Esse plus é frequentemente citado como a essência da automação de testes, mas vamos ver de um ângulo menos familiar. Que novas oportunidades ele abre para os desenvolvedores?

Primeiramente, cada novo desenvolvedor que veio ao projeto poderá executar facilmente testes existentes para entender a lógica do aplicativo usando exemplos. Infelizmente, a importância disso é muito subestimada. Em condições modernas, as mesmas pessoas raramente trabalham em um projeto por mais de um a dois anos. E como as equipes são compostas por várias pessoas, a aparência de um novo participante a cada 2-3 meses é uma situação típica para projetos relativamente grandes. Projetos particularmente difíceis estão passando por mudanças de gerações inteiras de desenvolvedores! A capacidade de iniciar facilmente qualquer parte do aplicativo e observar o comportamento do sistema às vezes simplifica a imersão de novos programadores no projeto. Além disso, um estudo mais detalhado da lógica do código reduz o número de erros cometidos na saída e o tempo para depurá-los no futuro.

Em segundo lugar, a capacidade de verificar facilmente se o aplicativo está funcionando corretamente abre caminho para a refatoração contínua. Infelizmente, esse termo é muito menos popular que o IC. Isso significa que a refatoração pode e deve ser feita sempre que o código é refinado. É o cumprimento regular da notória regra dos escoteiros “deixe o estacionamento mais limpo do que era antes da sua chegada”, o que permite evitar a degradação do código base e garante ao projeto uma vida longa e feliz.

Depuração


A depuração já foi mencionada nos parágrafos anteriores, mas esse ponto é tão importante que merece uma análise mais detalhada. Infelizmente, não há uma maneira confiável de medir a relação entre o tempo gasto escrevendo e depurando o código, pois esses processos são praticamente inseparáveis ​​um do outro. No entanto, a presença de testes de qualidade no projeto reduz significativamente o tempo de depuração, até a quase completa ausência da necessidade de executar um depurador.

Eficácia


Todas as opções acima podem economizar significativamente tempo na depuração inicial do código. Com a abordagem correta, somente isso pagará todos os custos adicionais de desenvolvimento. Os bônus de teste restantes - melhorando a qualidade da base de código (código mal projetado é difícil de testar), reduzindo o número de defeitos, a capacidade de verificar a correção do código a qualquer momento etc. - ficarão quase gratuitos.

Da teoria à prática


Em palavras, tudo parece bom, mas vamos ao que interessa. Como mencionado anteriormente, existem materiais mais do que suficientes sobre como fazer a configuração inicial do ambiente de teste. Portanto, prosseguimos imediatamente para o projeto finalizado. Fontes aqui.

Desafio


Como uma tarefa de modelo, considere um pequeno fragmento do back-end de uma loja online. Escreveremos uma API típica para trabalhar com produtos: criar, receber, editar. Além de alguns métodos para trabalhar com os clientes: alterar um "produto favorito" e calcular pontos de bônus para um pedido.

Modelo de domínio


Para não sobrecarregar o exemplo, nos restringimos a um conjunto mínimo de campos e classes.



O cliente possui um nome de usuário, um link para um produto favorito e um sinalizador indicando se é um cliente premium.

Produto (Produto) - nome, preço, desconto e sinalizador indicando se é atualmente anunciado.

Estrutura do projeto


A estrutura do código principal do projeto é a seguinte.



As classes são em camadas:

  • Modelo - modelo de domínio do projeto;
  • Jpa - repositórios para trabalhar com bancos de dados baseados em Spring Data;
  • Serviço - lógica de negócios da aplicação;
  • Controlador - controladores que implementam a API.

Estrutura de teste de unidade.



As classes de teste estão nos mesmos pacotes que o código original. Além disso, foi criado um pacote com construtores para a preparação dos dados de teste, mas mais sobre isso abaixo.

É conveniente separar testes de unidade e testes de integração. Eles geralmente têm dependências diferentes e, para um desenvolvimento confortável, deve haver a capacidade de executar um ou outro. Isso pode ser alcançado de várias maneiras: convenções de nomenclatura, módulos, pacotes, sourceSets. A escolha de um método específico é exclusivamente uma questão de gosto. Neste projeto, os testes de integração estão em um sourceSet - integrationTest separado.



Como os testes de unidade, as classes com testes de integração estão nos mesmos pacotes que o código original. Além disso, existem classes base que ajudam a se livrar da duplicação de configuração e, se necessário, contêm métodos universais úteis.

Testes de integração


Existem diferentes abordagens para quais testes valem a pena começar. Se a lógica testada não for muito complicada, você poderá prosseguir imediatamente para as de integração (elas também são chamadas de aceitação). Ao contrário dos testes de unidade, eles garantem que o aplicativo como um todo funcione corretamente.

Arquitetura

Primeiro, você precisa decidir em que nível específico as verificações de integração serão executadas. O Spring Boot oferece total liberdade de escolha: você pode elevar parte do contexto, todo o contexto e até um servidor completo, acessível a partir dos testes. À medida que o tamanho do aplicativo aumenta, esse problema se torna cada vez mais complexo. Muitas vezes, você precisa escrever testes diferentes em diferentes níveis.

Um bom ponto de partida seria o teste do controlador sem iniciar o servidor. Em aplicações relativamente pequenas, é aceitável elevar todo o contexto, pois, por padrão, é reutilizado entre testes e inicializado apenas uma vez. Considere os métodos básicos da classe ProductController :

 @PostMapping("new") public Product createProduct(@RequestBody Product product) { return productService.createProduct(product); } @GetMapping("{productId}") public Product getProduct(@PathVariable("productId") long productId) { return productService.getProduct(productId); } @PostMapping("{productId}/edit") public void updateProduct(@PathVariable("productId") long productId, @RequestBody Product product) { productService.updateProduct(productId, product); } 

A questão do tratamento de erros é deixada de lado. Suponha que ele seja implementado externamente com base em uma análise de exceções lançadas. O código dos métodos é muito simples, sua implementação no ProductService não ProductService muito mais complicada:

 @Transactional(readOnly = true) public Product getProduct(Long productId) { return productRepository.findById(productId) .orElseThrow(() -> new DataNotFoundException("Product", productId)); } @Transactional public Product createProduct(Product product) { return productRepository.save(new Product(product)); } @Transactional public Product updateProduct(Long productId, Product product) { Product dbProduct = productRepository.findById(productId) .orElseThrow(() -> new DataNotFoundException("Product", productId)); dbProduct.setPrice(product.getPrice()); dbProduct.setDiscount(product.getDiscount()); dbProduct.setName(product.getName()); dbProduct.setIsAdvertised(product.isAdvertised()); return productRepository.save(dbProduct); } 

O repositório ProductRepository não contém seus próprios métodos:

 public interface ProductRepository extends JpaRepository<Product, Long> { } 

Tudo indica que essas classes não precisam de testes de unidade simplesmente porque toda a cadeia pode ser verificada com facilidade e eficiência por vários testes de integração. A duplicação dos mesmos testes em testes diferentes complica a depuração. No caso de um erro no código, agora nenhum teste cairá, mas 10 a 15 de uma vez. Por sua vez, isso exigirá uma análise mais aprofundada. Se não houver duplicação, é provável que o único teste reprovado indique imediatamente um erro.

Configuração

Por conveniência, destacamos a classe base BaseControllerIT , que contém a configuração Spring e alguns campos:

 @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) @Transactional public abstract class BaseControllerIT { @Autowired protected ProductRepository productRepository; @Autowired protected CustomerRepository customerRepository; } 

Os repositórios são movidos para a classe base para não confundir as classes de teste. Sua função é exclusivamente auxiliar: preparando dados e verificando o status do banco de dados após o controlador funcionar. Quando você aumenta o tamanho do aplicativo, isso pode não ser mais conveniente, mas, para começar, é bastante adequado.

A configuração principal do Spring é definida pelas seguintes linhas:

@SpringBootTest - usado para definir o contexto do aplicativo. WebEnvironment.NONE significa que nenhum contexto da web precisa ser gerado.

@Transactional - @Transactional todos os testes de classe em uma transação com reversão automática para salvar o estado do banco de dados.

Estrutura de teste

Vamos passar para um conjunto minimalista de testes para a classe ProductControllerIT - ProductControllerIT .

 @Test public void createProduct_productSaved() { Product product = product("productName").price("1.01").discount("0.1").advertised(true).build(); Product createdProduct = productController.createProduct(product); Product dbProduct = productRepository.getOne(createdProduct.getId()); assertEquals("productName", dbProduct.getName()); assertEquals(number("1.01"), dbProduct.getPrice()); assertEquals(number("0.1"), dbProduct.getDiscount()); assertEquals(true, dbProduct.isAdvertised()); } 

O código do teste deve ser extremamente simples e compreensível à primeira vista. Caso contrário, a maioria das vantagens dos testes descritos na primeira seção do artigo são perdidas. É uma boa prática dividir o corpo de teste em três partes que podem ser visualmente separadas uma da outra: preparar dados, chamar o método de teste, validar os resultados. Ao mesmo tempo, é muito desejável que o código de teste caiba na tela como um todo.

Pessoalmente, parece-me mais óbvio quando os valores de teste da seção de preparação de dados são usados ​​posteriormente nas verificações. Como alternativa, você pode comparar explicitamente objetos, por exemplo, assim:

 assertEquals(product, dbProduct); 

Em outro teste para atualizar as informações do produto ( updateProduct ), fica claro que a criação de dados se tornou um pouco mais complicada e, para manter a integridade visual das três partes do teste, eles são separados por dois feeds de linha seguidos:

 @Test public void updateProduct_productUpdated() { Product product = product("productName").build(); productRepository.save(product); Product updatedProduct = product("updatedName").price("1.1").discount("0.5").advertised(true).build(); updatedProduct.setId(product.getId()); productController.updateProduct(product.getId(), updatedProduct); Product dbProduct = productRepository.getOne(product.getId()); assertEquals("updatedName", dbProduct.getName()); assertEquals(number("1.1"), dbProduct.getPrice()); assertEquals(number("0.5"), dbProduct.getDiscount()); assertEquals(true, dbProduct.isAdvertised()); } 

Cada uma das três partes da massa pode ser simplificada. Para a preparação dos dados, os construtores de teste são excelentes, os quais contêm a lógica para criar objetos que é conveniente para usar a partir de testes. Chamadas de método muito complexas podem ser transformadas em métodos auxiliares nas classes de teste, ocultando alguns dos parâmetros que são irrelevantes para essa classe. Para simplificar verificações complexas, você também pode escrever funções auxiliares ou implementar seus próprios correspondentes. O principal com todas essas simplificações é não perder a visibilidade do teste: tudo deve ficar claro rapidamente no método principal, sem a necessidade de aprofundar.

Construtores de teste

Os construtores de teste merecem atenção especial. Encapsular a lógica da criação de objetos simplifica a manutenção do teste. Em particular, o preenchimento dos campos do modelo que não são relevantes para este teste pode ser oculto dentro do construtor. Para fazer isso, não é necessário criá-lo diretamente, mas use um método estático que preencha os campos ausentes com valores padrão. Por exemplo, se novos campos obrigatórios aparecerem no modelo, eles poderão ser facilmente adicionados a esse método. No ProductBuilder fica assim:

 public static ProductBuilder product(String name) { return new ProductBuilder() .name(name) .advertised(false) .price("0.00"); } 

Nome do teste

É imperativo entender o que é testado especificamente neste teste. Para maior clareza, é melhor dar uma resposta a esta pergunta em seu título. Usando os testes de amostra para o método getProduct considere a convenção de nomenclatura usada:

 @Test public void getProduct_oneProductInDb_productReturned() { Product product = product("productName").build(); productRepository.save(product); Product result = productController.getProduct(product.getId()); assertEquals("productName", result.getName()); } @Test public void getProduct_twoProductsInDb_correctProductReturned() { Product product1 = product("product1").build(); Product product2 = product("product2").build(); productRepository.save(product1); productRepository.save(product2); Product result = productController.getProduct(product1.getId()); assertEquals("product1", result.getName()); } 

No caso geral, o cabeçalho do método de teste consiste em três partes, separadas por sublinhado: o nome do método que está sendo testado, o script e o resultado esperado. No entanto, ninguém cancelou o bom senso e pode ser justificado omitir algumas partes do nome se elas não forem necessárias nesse contexto (por exemplo, um script em um único teste para criar um produto). O objetivo dessa nomeação é garantir que a essência de cada teste seja compreensível sem aprender o código. Isso torna a janela dos resultados do teste o mais clara possível e é com ela que o trabalho com os testes geralmente começa.



Conclusões

Isso é tudo. Pela primeira vez, um conjunto mínimo de quatro testes é suficiente para testar os métodos da classe ProductController . No caso de detecção de erros, você sempre pode adicionar os testes ausentes. Ao mesmo tempo, o número mínimo de testes reduz significativamente o tempo e o esforço para apoiá-los. Isso, por sua vez, é fundamental no processo de implementação dos testes, uma vez que os primeiros testes geralmente não são da melhor qualidade e criam muitos problemas inesperados. Ao mesmo tempo, esse conjunto de testes é suficiente para receber os bônus descritos na primeira parte do artigo.

Vale ressaltar que esses testes não verificam a camada da web do aplicativo, mas geralmente isso não é necessário. Se necessário, você pode escrever testes separados para a camada da Web com um esboço em vez da base ( @WebMvcTest , MockMvc , @MockBean ) ou usar um servidor completo. O último pode complicar a depuração e o trabalho com transações, pois o teste não pode controlar a transação do servidor. Um exemplo desse teste de integração pode ser encontrado na classe CustomerControllerServerIT .

Testes unitários


Os testes de unidade têm várias vantagens sobre os testes de integração:

  • A inicialização leva milissegundos;
  • Tamanho pequeno da unidade testada;
  • É fácil implementar a verificação de um grande número de opções, pois quando o método é chamado diretamente, a preparação dos dados é bastante simplificada.

Apesar disso, os testes de unidade, por natureza, não podem garantir a operacionalidade do aplicativo como um todo e não permitem que você evite gravar os de integração. Se a lógica da unidade em teste for simples, a duplicação de testes de integração com testes de unidade não trará nenhum benefício, mas apenas adicionará mais código ao suporte.

A única classe neste exemplo que merece teste de unidade é o BonusPointCalculator . Sua característica distintiva é um grande número de ramos da lógica de negócios. Por exemplo, supõe-se que o comprador receba bônus de 10% do custo do produto, multiplicado por não mais de 2 multiplicadores da lista a seguir:

  • O produto custa mais de 10.000 (× 4);
  • O produto participa de uma campanha publicitária (× 3);
  • O produto é o produto "favorito" do cliente (× 5);
  • O cliente tem um status premium (× 2);
  • Se o cliente tiver um status premium e comprar um produto “favorito”, em vez dos dois multiplicadores indicados, um (× 8) será usado.

Na vida real, é claro, valeria a pena projetar um mecanismo universal flexível para calcular esses bônus, mas, para simplificar o exemplo, nos restringimos a uma implementação fixa. O código de cálculo do multiplicador é assim:

 private List<BigDecimal> calculateMultipliers(Customer customer, Product product) { List<BigDecimal> multipliers = new ArrayList<>(); if (customer.getFavProduct() != null && customer.getFavProduct().equals(product)) { if (customer.isPremium()) { multipliers.add(PREMIUM_FAVORITE_MULTIPLIER); } else { multipliers.add(FAVORITE_MULTIPLIER); } } else if (customer.isPremium()) { multipliers.add(PREMIUM_MULTIPLIER); } if (product.isAdvertised()) { multipliers.add(ADVERTISED_MULTIPLIER); } if (product.getPrice().compareTo(EXPENSIVE_THRESHOLD) >= 0) { multipliers.add(EXPENSIVE_MULTIPLIER); } return multipliers; } 

Um grande número de opções leva ao fato de que dois ou três testes de integração não são limitados aqui. Um conjunto minimalista de testes de unidade é perfeito para depurar essa funcionalidade.



O conjunto de testes correspondente pode ser encontrado na classe BonusPointCalculatorTest . Aqui estão alguns deles:

 @Test public void calculate_oneProduct() { Product product = product("product").price("1.00").build(); Customer customer = customer("customer").build(); Map<Product, Long> quantities = mapOf(product, 1L); BigDecimal bonus = bonusPointCalculator.calculate(customer, list(product), quantities::get); BigDecimal expectedBonus = bonusPoints("0.10").build(); assertEquals(expectedBonus, bonus); } @Test public void calculate_favProduct() { Product product = product("product").price("1.00").build(); Customer customer = customer("customer").favProduct(product).build(); Map<Product, Long> quantities = mapOf(product, 1L); BigDecimal bonus = bonusPointCalculator.calculate(customer, list(product), quantities::get); BigDecimal expectedBonus = bonusPoints("0.10").addMultiplier(FAVORITE_MULTIPLIER).build(); assertEquals(expectedBonus, bonus); } 

Vale ressaltar que nos testes nos referimos especificamente à API pública da classe - o método de calculate . Testar um contrato de classe em vez de sua implementação evita quebras de testes devido a alterações não funcionais e refatoração.

Finalmente, quando verificamos a lógica interna com testes de unidade, não precisamos mais colocar todos esses detalhes em integração. Nesse caso, basta um teste mais ou menos representativo, por exemplo:

 @Test public void calculateBonusPoints_twoProductTypes_correctValueCalculated() { Product product1 = product("product1").price("1.01").build(); Product product2 = product("product2").price("10.00").build(); productRepository.save(product1); productRepository.save(product2); Customer customer = customer("customer").build(); customerRepository.save(customer); Map<Long, Long> quantities = mapOf(product1.getId(), 1L, product2.getId(), 2L); BigDecimal bonus = customerController.calculateBonusPoints( new CalculateBonusPointsRequest("customer", quantities) ); BigDecimal bonusPointsProduct1 = bonusPoints("0.10").build(); BigDecimal bonusPointsProduct2 = bonusPoints("1.00").quantity(2).build(); BigDecimal expectedBonus = bonusPointsProduct1.add(bonusPointsProduct2); assertEquals(expectedBonus, bonus); } 

Como no caso de testes de integração, o conjunto de testes de unidade usado é muito pequeno e não garante a total correção da aplicação. No entanto, sua presença aumenta significativamente a confiança no código, facilita a depuração e concede os outros bônus listados na primeira parte do artigo.

Recomendações de implementação


Espero que as seções anteriores tenham sido suficientes para convencer pelo menos um desenvolvedor a tentar começar a usar testes em seu projeto. Este capítulo listará brevemente as principais recomendações que ajudarão a evitar problemas sérios e reduzirão os custos iniciais de implementação.

Tente começar a implementar os testes no novo aplicativo. Escrever os primeiros testes em um grande projeto legado será muito mais difícil e exigirá mais habilidade do que em um recém-criado. Portanto, se possível, é melhor começar com um novo aplicativo pequeno. Se novos aplicativos completos não forem esperados, você pode tentar desenvolver algum utilitário útil para uso interno. O principal é que a tarefa deve ser mais ou menos realista - exemplos inventados não fornecerão uma experiência completa.

Configure execuções de teste regulares. Se os testes não forem executados regularmente, eles não apenas param de executar sua função principal - verificar a correção do código - mas também rapidamente se tornam desatualizados. Portanto, é extremamente importante configurar pelo menos o pipeline mínimo de IC com o lançamento automático de testes sempre que o código for atualizado no repositório.

Não persiga a tampa. Como no caso de qualquer outra tecnologia, a princípio os testes não serão obtidos da melhor qualidade. A literatura relevante (links no final do artigo) ou um mentor competente pode ajudar aqui, mas isso não cancela a necessidade de cones autoadesivos. Os testes nesse sentido são semelhantes ao restante do código: para entender como eles afetarão o projeto, é possível somente depois de viver com eles por um tempo. Portanto, para minimizar os danos, a primeira vez é melhor não perseguir o número e números bonitos como cem por cento de cobertura. Em vez disso, você deve limitar-se aos principais cenários positivos para a sua própria funcionalidade de aplicativo.

Não se deixe levar pelos testes de unidade. Continuando o tema “quantidade versus qualidade”, deve-se notar que testes de unidade honestos não devem ser realizados pela primeira vez, porque isso pode facilmente levar a especificações excessivas da aplicação. Por sua vez, isso se tornará um sério fator inibidor nas subsequentes melhorias na refatoração e aplicação. Os testes de unidade devem ser usados ​​apenas se houver lógica complexa em uma classe ou grupo de classes específico, o que é inconveniente para verificar no nível de integração.

Não se empolgue com classes de stub e métodos de aplicação. Os stubs (stub, mock) são outra ferramenta que requer uma abordagem equilibrada e a manutenção de um equilíbrio. Por um lado, o isolamento completo da unidade permite que você se concentre na lógica testada e não pense no resto do sistema. Por outro lado, isso exigirá tempo de desenvolvimento adicional e, como nos testes de unidade, pode levar à especificação excessiva de comportamento.

Desatar testes de integração de sistemas externos. Um erro muito comum nos testes de integração é o uso de um banco de dados real, filas de mensagens e outros sistemas externos ao aplicativo. Obviamente, a capacidade de executar um teste em um ambiente real é útil para depuração e desenvolvimento. Tais testes em pequenas quantidades podem fazer sentido, especialmente para execução interativa. No entanto, seu uso generalizado leva a vários problemas:

  1. Para executar os testes, você precisará configurar o ambiente externo. Por exemplo, instale um banco de dados em cada máquina em que o aplicativo será montado. Isso dificultará a entrada de novos desenvolvedores no projeto e a configuração do IC.
  2. O estado dos sistemas externos pode variar em máquinas diferentes antes de executar os testes. Por exemplo, o banco de dados já pode conter as tabelas que o aplicativo precisa com dados que não são esperados no teste. Isso levará a falhas imprevisíveis nos testes e sua eliminação exigirá uma quantidade significativa de tempo.
  3. Se houver trabalho paralelo em vários projetos, é possível a influência não óbvia de alguns projetos em outros. Por exemplo, configurações específicas do banco de dados feitas para um dos projetos podem ajudar a funcionalidade de outro projeto a funcionar corretamente, o que, no entanto, será interrompido quando iniciado em um banco de dados limpo em outra máquina.
  4. Os testes são realizados por um longo tempo: uma execução completa pode chegar a dezenas de minutos. Isso leva ao fato de que os desenvolvedores param de executar testes localmente e analisam seus resultados somente após enviar as alterações para o repositório remoto. Esse comportamento nega a maioria das vantagens dos testes, discutidos na primeira parte do artigo.

Limpe o contexto entre testes de integração. Freqüentemente, para acelerar o trabalho de testes de integração, é necessário reutilizar o mesmo contexto entre eles. Até a documentação oficial do Spring faz essa recomendação. Ao mesmo tempo, a influência dos testes um no outro deve ser evitada. Como são lançadas em uma ordem arbitrária, a presença de tais conexões pode levar a erros irreprodutíveis aleatórios. Para impedir que isso aconteça, os testes não devem deixar para trás nenhuma alteração no contexto. Por exemplo, ao usar um banco de dados, para isolamento, geralmente é suficiente reverter todas as transações confirmadas no teste. Se não for possível evitar alterações no contexto, você poderá configurar sua recreação usando a anotação @DirtiesContext .

, . , - . , . , , — , .

. , , . , , .

TDD (Test-Driven Development). TDD , , . , , . , , .

, ?


, :

  1. ( )? .
  2. , ( , CI)? .
  3. ? .
  4. ? . , , .

, . , , - . — .

Conclusão


, . - , . , - . — , , -. , .

, , , !

GitHub

Source: https://habr.com/ru/post/pt427603/


All Articles