O Spring Framework é frequentemente citado como um exemplo do framework Cloud Native , projetado para funcionar na nuvem, desenvolver aplicativos de doze fatores , microsserviços e um dos produtos mais estáveis, mas ao mesmo tempo inovadores. Mas neste artigo, gostaria de me debruçar sobre um lado mais forte do Spring: é o suporte ao desenvolvimento por meio de testes (capacidade TDD?). Apesar da conectividade TDD, notei com frequência que os projetos Spring ignoram algumas práticas recomendadas para teste, inventam suas próprias bicicletas ou não escrevem testes porque são "lentos" ou "não confiáveis". E explicarei exatamente como escrever testes rápidos e confiáveis para aplicativos no Spring Framework e conduzir o desenvolvimento por meio de testes. Portanto, se você usa o Spring (ou deseja iniciar), entenda o que são testes em geral (ou deseja entender) ou pense que contextLoads
é o nível necessário e suficiente de testes de integração - será interessante!
O recurso "TDD" é muito ambíguo e pouco mensurável, mas, no entanto, o Spring tem muitas coisas que, por design, ajudam a escrever testes de integração e unidade com um mínimo de esforço. Por exemplo:
- Teste de integração - você pode facilmente iniciar o aplicativo, bloquear componentes, redefinir parâmetros etc.
- Teste de integração de foco - somente acesso a dados, apenas web etc.
- Suporte pronto para uso - bancos de dados em memória, filas de mensagens, autenticação e autorização em testes
- Teste através de contratos (Spring Cloud Contract)
- Suporte à Web UI Testing usando HtmlUnit
- Flexibilidade de configuração de aplicativos - perfis, configurações de teste, componentes etc.
- E muito mais
Para começar, uma pequena mas necessária introdução sobre TDD e testes em geral.
Desenvolvimento orientado a testes
O TDD é baseado em uma idéia muito simples - escrevemos testes antes de escrever código. Em teoria, parece assustador, mas depois de algum tempo chega a compreensão de práticas e técnicas, e a opção de escrever testes depois causa desconforto tangível. Uma das principais práticas é a iteração , ou seja, faça tudo iterações pequenas e focadas, cada uma delas descrita como um refator vermelho-verde .
Na fase vermelha , escrevemos um teste de queda, e é muito importante que ele caia com uma razão e descrição claras e compreensíveis, e que o teste em si seja completo e passe quando o código for escrito. O teste deve verificar o comportamento , não a implementação , ou seja, siga a abordagem da caixa preta, então vou explicar o porquê.
Na fase verde , escrevemos o código mínimo necessário para passar no teste. Às vezes, é interessante praticar e deixá-lo o mais louco possível (embora seja melhor não se deixar levar) e quando uma função retorna um booleano dependendo do estado do sistema, o primeiro "passe" pode ser simplesmente return true
.
Na fase de refatoração , que só pode ser iniciada quando todos os testes estiverem em verde , refatoraremos o código e o colocaremos em condições adequadas. Nem é necessário para um pedaço de código que escrevemos, portanto, é importante começar a refatorar em um sistema estável. A abordagem de "caixa preta" ajudará apenas a refatoração, alterando a implementação, mas não afetando o comportamento.
Falarei sobre diferentes aspectos do TDD no futuro, afinal, essa é a ideia de uma série de artigos, então agora não vou me concentrar nos detalhes. Mas, antes de responder às críticas padrão do TDD, mencionarei alguns mitos que ouço com frequência.
- "TDD é cerca de 100% de cobertura do código, mas não oferece garantias" - o desenvolvimento através de testes não tem nenhuma relação com 100% de cobertura. Em muitas equipes em que trabalhei, essa métrica nem sequer foi medida e foi classificada como vaidade. E sim, 100% da cobertura do teste não significa nada.
- "O TDD funciona apenas para funções simples; um aplicativo real com um banco de dados e um estado difícil não podem ser criados com ele" é uma desculpa muito popular, geralmente complementada por "Temos um aplicativo tão complicado que não escrevemos testes, você não pode fazê-lo". Eu vi uma abordagem TDD funcional em aplicativos completamente diferentes - Web (com e sem SPA), móvel, API, microsserviços, monólitos, sistemas bancários complexos, plataformas em nuvem, estruturas, plataformas de varejo escritas em diferentes idiomas e tecnologias. Portanto, o mito popular “Somos únicos, tudo é diferente” costuma ser uma desculpa para não investir esforço e dinheiro em testes, mas não é um motivo real (embora também possa haver motivos reais).
- "Ainda haverá erros no TDD" - é claro, como em qualquer outro software. TDD não é sobre bugs ou sua ausência, é uma ferramenta de desenvolvimento. Como depuração. Como um IDE. Como a documentação. Nenhuma dessas ferramentas garante a ausência de bugs, elas apenas ajudam a lidar com a crescente complexidade do sistema.
O principal objetivo do TDD e geralmente testar é dar à equipe a confiança de que o sistema está funcionando de maneira estável. Portanto, nenhuma das práticas de teste determina quantos e quais testes gravar. Escreva quanto você acha necessário, quanto você precisa ter certeza de que agora o código pode ser colocado em produção e funcionará . Há pessoas que consideram os testes de integração rápida como uma caixa preta ultimativa necessária e suficiente, e os testes de unidade opcionais. Alguém diz que o e2e testa com a possibilidade de uma reversão rápida para a versão anterior e a presença de lançamentos de canários não é tão crítica. Quantas equipes - tantas abordagens, é importante encontrar a sua.
Um dos meus objetivos é me afastar do formato “desenvolvimento através do teste de uma função que adiciona dois números” na história do TDD e olhar para um aplicativo real, um tipo de prática de teste que foi evaporada para um aplicativo mínimo, coletada em projetos reais. Como um exemplo semi-real, usarei um pequeno aplicativo da Web que eu mesmo inventei para resumo fábricas Padaria - Fábrica de Bolos . Eu pretendo escrever pequenos artigos, concentrando-me cada vez em uma parte separada da funcionalidade do aplicativo e mostrar através do TDD que você pode projetar APIs, a estrutura interna do aplicativo e manter refatoração constante.
Um plano de amostra para uma série de artigos, como eu vejo no momento, é:
- Esqueleto ambulante - estrutura de aplicativo onde você pode executar o ciclo Refatorar Vermelho-Verde
- Teste de UI e design orientado a comportamento
- Teste de acesso a dados (Spring Data)
- Teste de autorização e autenticação (Spring Security)
- Jet Stack (WebFlux + Reator de Projeto)
- Interoperabilidade de (micro) serviços e contratos (Spring Cloud)
- Testando o serviço de enfileiramento de mensagens (Spring Cloud)
Este artigo introdutório abordará os pontos 1 e 2 - vou criar uma estrutura de aplicativo e um teste básico de interface do usuário usando a abordagem BDD - ou desenvolvimento orientado a comportamento . Cada artigo começará com uma história de usuário , mas não falarei sobre a parte "produto" para economizar tempo. A história do usuário será escrita em inglês, logo ficará claro o porquê. Todos os exemplos de código podem ser encontrados no GitHub, portanto, não analisarei todo o código, apenas as partes importantes.
A história do usuário é uma descrição de um recurso de um aplicativo de linguagem natural que geralmente é escrito em nome de um usuário do sistema.
História do usuário 1: o usuário vê a página de boas-vindas
Como Alice, uma nova usuária
Quero ver uma página de boas-vindas ao visitar o site Cake Factory
Para que eu saiba quando a Cake Factory está prestes a lançar
Critérios de aceitação:
Cenário: um usuário que visita a visita ao site antes da data de lançamento
Dado que eu sou um novo usuário
Quando visito o site da Cake Factory
Então, vejo a mensagem "Obrigado pelo seu interesse"
E vejo uma mensagem 'O site está chegando em breve ...'
É preciso conhecimento: o que é Desenvolvimento Orientado a Comportamento e Pepino , os princípios básicos do Spring Boot Testing .
A primeira história do usuário é bastante básica, mas o objetivo ainda não é complexo, mas na criação do esqueleto ambulante - um aplicativo mínimo para iniciar o ciclo TDD .
Depois de criar um novo projeto no Spring Initializr com módulos Web e Bigode, para começar, precisarei de mais algumas alterações para build.gradle
:
- adicione
testImplementation('net.sourceforge.htmlunit:htmlunit')
HtmlUnit testImplementation('net.sourceforge.htmlunit:htmlunit')
. Você não precisa especificar a versão, o plug-in de gerenciamento de dependência do Spring Boot para Gradle selecionará automaticamente a versão necessária e compatível - migrar um projeto da JUnit 4 para a JUnit 5 (porque 2018 está no quintal)
- adicionar dependências ao Cucumber - uma biblioteca que eu usarei para escrever especificações de BDD
- remover criado por padrão
CakeFactoryApplicationTests
com contextLoads
inevitável
Em geral, este é o "esqueleto" básico do aplicativo, você já pode escrever o primeiro teste.
Para facilitar a navegação no código, falarei brevemente sobre as tecnologias usadas.
Pepino
O pepino é uma estrutura de desenvolvimento orientada a comportamento que ajuda a criar "especificações executáveis", ou seja, executar testes (especificações) escritos em linguagem natural. O plug-in Cucumber analisa o código-fonte em Java (e muitas outras linguagens) e usa definições de etapas para executar o código real. As definições de etapa são métodos de classe anotados por @Given
, @When
, @Then
e outras anotações.
Unidade html
A home page do projeto chama o HtmlUnit "de um navegador sem GUI para aplicativos Java". Diferentemente do Selenium, o HtmlUnit não inicia um navegador real e, mais importante, não renderiza a página, trabalhando diretamente com o DOM. O JavaScript é suportado pelo mecanismo Mozilla Rhino. O HtmlUnit é adequado para aplicativos clássicos, mas não muito amigável com os aplicativos de página única. Para começar, será suficiente e tentarei mostrar que mesmo coisas como uma estrutura de teste podem fazer parte da implementação, e não a base do aplicativo.
Primeiro teste
Agora, uma história de usuário escrita em inglês será útil para mim. O melhor gatilho para iniciar a próxima iteração TDD é o critério de aceitação escrito de tal maneira que eles possam ser transformados em uma especificação executável com um mínimo de gestos.
Idealmente, as histórias do usuário devem ser escritas para que possam ser simplesmente copiadas para a especificação do BDD e executadas. Isso está longe de ser sempre simples e nem sempre possível, mas esse deve ser o objetivo do proprietário do produto e de toda a equipe, embora nem sempre seja possível.
Então, meu primeiro longa.
Feature: Welcome page Scenario: a user visiting the web-site visit before the launch date Given a new user, Alice When she visits Cake Factory web-site Then she sees a message 'Thank you for your interest' And she sees a message 'The web-site is coming in December!'
Se você gerar descrições de etapas (o plugin Intellij IDEA ajuda bastante no Gherkin) e executar o teste, é claro que será verde - ainda não testa nada. E aqui vem a fase importante do trabalho no teste - você precisa escrever um teste, como se o código principal tivesse sido escrito .
Freqüentemente, para quem começa a banir o TDD, um estupor se instala aqui - é difícil colocar na cabeça os algoritmos e a lógica de algo que ainda não existe. E, portanto, é muito importante ter iterações tão pequenas e focadas quanto possível, começando na história do usuário e indo para o nível de integração e unidade. É importante se concentrar em um teste de cada vez e tentar se molhar e ignorar dependências que ainda não são importantes. Às vezes, notei como as pessoas se afastam facilmente - crie uma interface ou classe para uma dependência, gere imediatamente uma classe de teste vazia, mais uma dependência é adicionada lá, outra interface é criada e assim por diante.
Se a história for "seria necessário atualizar o status ao salvar", é muito difícil automatizar e formalizar. No meu exemplo, cada etapa pode ser claramente definida em uma sequência de etapas que podem ser descritas por código. É claro que este é o exemplo mais simples e não mostra muito, mas espero que mais, com crescente complexidade, seja mais interessante.
Vermelho
Portanto, no meu primeiro recurso, criei as seguintes descrições de etapas:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class WelcomePage { private WebClient webClient; private HtmlPage page; @LocalServerPort private int port; private String baseUrl; @Before public void setUp() { webClient = new WebClient(); baseUrl = "http://localhost:" + port; } @Given("a new user, Alice") public void aNewUser() {
Alguns pontos a serem observados:
- recursos são iniciados por outro arquivo,
Features.java
usando a anotação RunWith
do JUnit 4, o Cucumber não suporta a versão 5, infelizmente @SpringBootTest
anotação @SpringBootTest
é adicionada à descrição das etapas, a cucumber-spring
pega a partir daí e configura o contexto de teste (ou seja, inicia o aplicativo)- O aplicativo Spring para o teste começa com
webEnvironment = RANDOM_PORT
e essa porta aleatória é passada para o teste usando @LocalServerPort
, o Spring encontrará essa anotação e definirá o valor do campo para a porta do servidor
E o teste, como esperado, falha com o erro 404 for http://localhost:51517
.
Os erros com os quais o teste trava são incrivelmente importantes, especialmente quando se trata de testes de unidade ou integração, e esses erros fazem parte da API. Se o teste falhar com uma NullPointerException
isso não é muito bom, mas a BaseUrl configuration property is not set
- muito melhor.
Verde
Para tornar o teste verde, adicionei um controlador base e exibi-lo com o mínimo de HTML:
@Controller public class IndexController { @GetMapping public String index() { return "index"; } }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Cake Factory</title> </head> <body> <h1>Thank you for your interest</h1> <h2>The web-site is coming in December!</h2> </body> </html>
O teste é verde, o aplicativo funciona, embora seja feito na tradição de projetos de engenharia severos.
Em um projeto real e em uma equipe equilibrada , é claro, eu me sentava com o designer e transformavamos o HTML puro em algo muito mais bonito. Mas, no âmbito do artigo, um milagre não acontecerá, a princesa continuará sendo um sapo.
A pergunta “que parte do TDD é design” não é tão simples. Uma das práticas que achei úteis é a princípio nem olhar para a interface do usuário (nem mesmo executar o aplicativo para poupar os nervos), escrever um teste, torná-lo verde - e, em seguida, ter uma base estável, trabalhar no front-end, reiniciar constantemente os testes .
Refatorar
Na primeira iteração, não há refatoração específica, mas, embora eu tenha passado os últimos 10 minutos escolhendo um modelo para o Bulma , que pode ser contado como refatoração!
Em conclusão
Embora o aplicativo não tenha trabalho de segurança, nem banco de dados, nem API, os testes e TDDs parecem bastante simples. E, em geral, a partir da pirâmide de testes, toquei apenas no topo, o teste da interface do usuário. Mas nisso, em parte, o segredo da abordagem enxuta é fazer tudo em pequenas iterações, um componente de cada vez. Isso ajuda a focar nos testes, simplificá-los e controlar a qualidade do código. Espero que nos artigos a seguir sejam mais interessantes.
Referências
PS: O título do artigo não é tão louco quanto parece no começo, acho que muitos já imaginaram. "Como construir uma pirâmide na sua bota" é uma referência à pirâmide de teste (falarei mais sobre isso mais tarde) e ao Spring Boot, onde boot no inglês britânico também significa "trunk".