Escolar Documentos
Profissional Documentos
Cultura Documentos
Alan Mellor
BIRMINGHAM-MUMBAI
Machine Translated by Google
Todos os direitos reservados. Nenhuma parte deste livro pode ser reproduzida, armazenada em um sistema de
recuperação ou transmitida de qualquer forma ou por qualquer meio, sem a permissão prévia por escrito do editor,
exceto no caso de breves citações incorporadas em artigos críticos ou resenhas.
Todos os esforços foram feitos na preparação deste livro para garantir a precisão das informações apresentadas.
Contudo, as informações contidas neste livro são vendidas sem garantia, expressa ou implícita. Nem o(s) autor(es),
nem a Packt Publishing ou seus revendedores e distribuidores serão responsabilizados por quaisquer danos causados
ou supostamente causados direta ou indiretamente por este livro.
A Packt Publishing tem se esforçado para fornecer informações de marcas registradas sobre todas as empresas e
produtos mencionados neste livro através do uso apropriado de letras maiúsculas. No entanto, a Packt Publishing não
pode garantir a exatidão desta informação.
Lugar de libré
Rua Livery, 35
Birmingham
B3 2PB, Reino Unido.
ISBN 978-1-80323-623-0
www.packt.com
Machine Translated by Google
Em memória da minha mãe, Eva Mellor (1928 – 2022). Você me viu começar este livro, mas não terminá-lo.
Para ser totalmente honesto, você não teria gostado tanto quanto seus romances de Georgette Heyer.
–Alan Mellor
Machine Translated by Google
Colaboradores
Sobre o autor
Alan Mellor é líder da academia BJSS, treinando a próxima geração de engenheiros de software consultores,
e autor de Java OOP Done Right: Crie código orientado a objetos do qual você possa se orgulhar com Java
moderno. Alan começou com um computador Sinclair ZX81 com 1K de RAM e está muito feliz por ter
computadores melhores agora. O trabalho de Alan inclui controle industrial em C, aplicações web para comércio
eletrônico, jogos e serviços bancários em Java e Go, e armazenamento de documentos em C++. Seu código
mais visível faz parte do Nokia Bounce e do simulador de vôo RAF Red Arrows, se você voltar o suficiente.
Quero agradecer à minha esposa Stephanie, sem cujo apoio este livro não teria sido possível. Sou grato
a todos que me ensinaram engenharia de software, seja pessoalmente ou por meio de seus livros.
Todo o meu amor para Jake e Katy. Vocês dois são incríveis.
Machine Translated by Google
Sobre os revisores
Jeff Langr desenvolve software profissionalmente há mais de 4 décadas. Ele é reconhecido como
autor de cinco livros sobre desenvolvimento de software, incluindo Modern C++ Programming with
Test-Driven Development: Code Better, Sleep Better, Agile in a Flash (com Tim Ottinger) e Agile in a
Flash: Speed-learning Agile Software Development . Jeff também é coautor do livro best-seller Clean
Code. Ele escreveu mais de cem artigos publicados e centenas de posts em seu site (https://langrsoft.com).
Nikolai Avteniev iniciou sua carreira profissional no JPMorgan Chase, participou do Extreme Programming
Pilot e aprendeu como aplicar desenvolvimento orientado a testes e integração contínua.
Depois de se formar em ciência da computação pela NYU, ele aproveitou a experiência de construir e
administrar uma equipe de desenvolvimento ágil para sistemas de risco em tempo real como um dos
engenheiros fundadores. Mais tarde, Nikolai ingressou na Intent Media, start-up AdTech de Nova York, e
depois passou a construir equipes de software e sistemas no LinkedIn (https://engineering.linkedin.com/blog/2017/08/
conhecendo Nikolai-avteniev).
Além disso, Nikolai ensina engenharia de software na City University of New York. Atualmente trabalha
na Stripe, ajudando a aumentar o PIB da internet com segurança.
Machine Translated by Google
Machine Translated by Google
Índice
Prefácio xv
1
Construindo o Caso para TDD 3
5
Entendendo por que código incorreto é escrito
Diminuindo o desempenho da equipe 12
2
Usando TDD para criar um bom código 17
Diga o que você quer dizer, fale sério o que você diz 18
Proteção contra defeitos futuros 27
Cuide dos detalhes em particular 19 28
Documentando nosso código
Evite complexidade acidental 20
Resumo 29
Revelando falhas de design 22 29
Perguntas e respostas
Analisando os benefícios de escrever testes antes do
Leitura adicional 30
código de produção 23
Machine Translated by Google
viii Índice
3
Dissipando mitos comuns sobre TDD 31
Índice ix
5
Escrevendo nosso primeiro teste 55
Requerimentos técnicos 55 Preservando o encapsulamento 64
62 Resumo 73
Detectando erros comuns
63 Perguntas e respostas 73
Afirmando exceções
Testando apenas métodos públicos 64
6
Seguindo os ritmos do TDD 75
Requerimentos técnicos 75 Escrevendo nossos próximos testes para Wordz 79
7
Projeto de condução – TDD e SOLID 95
Requerimentos técnicos 96 Manutenção futura simplificada 100
Guia de teste – nós orientamos o design 96 Contra-exemplo – molda o código que viola
PRS 101
SRP – blocos de construção simples 97
Aplicando SRP para simplificar manutenção futura 102
Muitas responsabilidades tornam o código mais difícil
Organizar testes para ter uma única responsabilidade 104
de trabalhar 99
x Índice
Requerimentos técnicos 119 Você não pode zombar sem injeção de dependência 131
Não teste a simulação 132
Os problemas que os colaboradores apresentam
120 Quando usar objetos simulados 132
para testes
Os desafios de testar Trabalhando com Mockito – uma biblioteca de
comportamento irrepetível 120 133
simulação popular
Os desafios de testar o tratamento de erros 121
Primeiros passos com o Mockito 133
Entendendo por que essas colaborações são 133
Escrevendo um esboço com Mockito
desafiadoras 121
Escrevendo uma simulação com Mockito 141
Compreender quando os testes duplos são Testando uma condição de erro no Wordz 145
apropriados 130 147
Resumo
Evitando o uso excessivo de objetos simulados 130
Perguntas e respostas 148
Não zombe de código que você não possui 130
Leitura adicional 148
Não zombe de objetos de valor 131
Índice XI
Visão geral dos componentes da arquitetura Substituindo os adaptadores por testes duplos 166
hexagonal A 155
Unidade testando unidades maiores 167
regra de ouro – o domínio nunca se conecta
159 Teste de unidade de histórias de usuários inteiras 167
diretamente aos
Decidindo o que nosso modelo de domínio precisa 160 Projetando o banco de dados e adaptadores de
números aleatórios 173
Escrevendo o código de domínio 164
Resumo 173
Decidindo o que deveria estar em
nosso modelo de domínio 164 Perguntas e respostas 174
10
PRIMEIROS Testes e a Pirâmide de Testes 175
xii Índice
11
Explorando TDD com Garantia de Qualidade 199
12
Teste primeiro, teste depois, teste nunca 213
Adicionando testes primeiro 213 Testes? Eles são para pessoas que não
214 sabem escrever código! 221
Test-first é uma ferramenta de design
Testes formam especificações executáveis 215 O que acontece se não testarmos durante o
desenvolvimento? 222
Test-first fornece métricas significativas de cobertura
de código 215
Testando de dentro para fora 222
Cuidado ao tornar uma métrica de cobertura de
Testando de fora para dentro 224
código um alvo 216
Cuidado ao escrever todos os testes antecipadamente 217 Definindo limites de teste com
arquitetura hexagonal 226
Escrever testes primeiro ajuda na entrega
contínua 218 De dentro para fora funciona bem com o modelo de domínio 226
Índice xiii
13
Conduzindo a camada de domínio 235
14
Conduzindo a camada de banco de dados 267
15
Conduzindo a camada da Web 283
XIV Índice
Índice 313
Prefácio
O software moderno é impulsionado por usuários que desejam novos recursos, sem defeitos, lançados rapidamente – uma tarefa
muito desafiadora. Os desenvolvedores individuais deram lugar a equipes de desenvolvimento que trabalham juntas em um único
produto de software. Os recursos são adicionados em ciclos iterativos curtos e depois liberados para produção com frequência –
às vezes diariamente.
Alcançar isso requer excelência no desenvolvimento. Devemos garantir que nosso software esteja sempre pronto
para ser implantado e livre de defeitos quando for lançado em produção. Deve ser fácil para nossos colegas
desenvolvedores trabalharem. O código deve ser fácil de ser entendido e alterado por qualquer pessoa. Quando a
equipe fizer essas alterações, devemos ter confiança de que nossos novos recursos funcionarão corretamente e de
que não quebramos nenhum recurso existente.
Este livro reúne técnicas comprovadas que ajudam a tornar isso realidade.
Este livro permitirá que você escreva código bem projetado e testado. Você terá a confiança de que seu código
funciona como você acha que deveria. Você terá a segurança de um conjunto cada vez maior de testes de execução
rápida, mantendo um olhar atento sobre toda a base de código enquanto a equipe faz alterações. Você aprenderá
como organizar seu código para evitar dificuldades causadas por sistemas externos, como serviços de pagamento
ou mecanismos de banco de dados. Você reduzirá sua dependência de formas mais lentas de teste.
Você escreverá código de maior qualidade, adequado para uma abordagem de entrega contínua.
O software moderno requer uma abordagem de desenvolvimento moderna. Ao final deste livro, você
terá dominado as técnicas para aplicar um.
Este livro destina-se principalmente a desenvolvedores familiarizados com os fundamentos da linguagem Java e
que desejam ser eficazes em uma equipe de desenvolvimento ágil de alto desempenho. As técnicas descritas
neste livro permitem que seu código seja entregue à produção com poucos defeitos e uma estrutura que pode
ser alterada com facilidade e segurança. Esta é a base técnica da agilidade.
Os primeiros capítulos do livro também serão úteis para líderes empresariais que desejam compreender os custos e
benefícios dessas abordagens antes de se comprometerem com elas.
Machine Translated by Google
xvi Prefácio
O Capítulo 1, Construindo o Caso do TDD, fornece uma compreensão dos benefícios que o TDD traz e como
chegamos até aqui.
O Capítulo 2, Usando TDD para criar um bom código, aborda algumas boas práticas gerais que nos ajudarão a criar um código
bem projetado à medida que aplicamos o TDD.
O Capítulo 3, Dissipando Mitos Comuns sobre TDD, é uma revisão das objeções comuns ao uso do TDD
que podemos encontrar, com sugestões para superá-las. Este capítulo é adequado para líderes
empresariais que possam ter reservas quanto à introdução de uma nova técnica no processo de desenvolvimento.
O Capítulo 4, Construindo um aplicativo usando TDD, envolve a configuração de nosso ambiente de desenvolvimento para
construir o aplicativo Wordz usando TDD. Ele analisa como trabalhar em iterações curtas usando histórias de usuários.
O Capítulo 5, Escrevendo nosso primeiro teste, apresenta os fundamentos do TDD com o modelo Arrange, Act e Assert.
Escrevendo o primeiro código de teste e produção para Wordz, veremos detalhadamente como o TDD promove uma etapa de
design antes de escrevermos o código. Consideraremos diversas opções e compensações e, em seguida, capturaremos essas
decisões em um teste.
O Capítulo 6, Seguindo os Ritmos do TDD, demonstra o ciclo de refatoração vermelho, verde como um ritmo de desenvolvimento.
Decidimos qual será o próximo teste a ser escrito, observamos sua falha, fazemos com que ele passe e então refinamos nosso
código para que seja seguro e simples para a equipe trabalhar no futuro.
O Capítulo 7, Conduzindo o Design – TDD e SOLID, baseia-se nos capítulos anteriores, mostrando como o TDD fornece
feedback rápido sobre nossas decisões de design, trazendo o SOLID para o grupo. Os princípios SOLID são um conjunto útil
de diretrizes para ajudar a projetar código orientado a objetos. Este capítulo é uma revisão desses princípios para que estejam
prontos para serem aplicados no restante do livro.
O Capítulo 8, Test Doubles – Stubs and Mocks, explica duas técnicas críticas que nos permitem trocar coisas que são difíceis
de testar por coisas que são mais fáceis de testar. Ao fazer isso, podemos colocar mais código em um teste de unidade TDD,
reduzindo nossa necessidade de testes de integração mais lentos.
O Capítulo 9, Arquitetura Hexagonal – Desacoplando Sistemas Externos, apresenta uma poderosa técnica de design que nos
permite dissociar totalmente sistemas externos, como bancos de dados e servidores web, de nossa lógica central.
Apresentaremos aqui os conceitos de portas e adaptadores. Isso simplifica o uso do TDD e, como benefício, proporciona
resiliência a quaisquer mudanças que nos sejam impostas por fatores externos.
O Capítulo 10, FIRST Tests e a Pirâmide de Testes, descreve a pirâmide de testes como um meio de pensar sobre os
diferentes tipos de testes necessários para testar completamente um sistema de software. Discutimos testes de unidade,
integração e ponta a ponta e as compensações entre cada tipo.
O Capítulo 11, Como o TDD se encaixa na garantia de qualidade, explora como, ao usar a automação de testes avançada,
conforme descrito neste livro, nossos engenheiros de controle de qualidade ficam livres de alguns dos laboriosos testes
detalhados que, de outra forma, teriam que fazer. Este capítulo analisa como os testes são agora um esforço de toda a equipe
durante o desenvolvimento e como podemos combinar melhor nossas habilidades.
Machine Translated by Google
Prefácio xvii
O Capítulo 12, Teste primeiro, teste depois, teste nunca, analisa algumas abordagens diferentes para testes com
base em quando escrevemos os testes e no que exatamente estamos testando. Isso nos ajudará a aumentar a
qualidade dos testes que produzimos ao aplicar o TDD.
O Capítulo 13, Conduzindo a camada de domínio, explica a aplicação de TDD, SOLID, a pirâmide de teste e a arquitetura
hexagonal ao código da camada de domínio do Wordz. Combinadas, essas técnicas nos permitem submeter a maior parte
da lógica do jogo a testes unitários rápidos.
O Capítulo 14, Conduzindo a camada de banco de dados, oferece orientação sobre como escrever o código do adaptador que se
conecta ao nosso banco de dados SQL, Postgres, agora que desacoplamos o código do banco de dados da camada de domínio.
Fazemos esse teste primeiro, escrevendo um teste de integração usando a estrutura de teste do Database Rider. O código
de acesso a dados é implementado usando a biblioteca JDBI.
O Capítulo 15, Conduzindo a camada da Web, termina como o capítulo final do livro, explicando como escrever uma API
REST HTTP que permite que nosso jogo Wordz seja acessado como um serviço da Web. Isso é feito primeiro, usando um
teste de integração escrito usando as ferramentas incorporadas à biblioteca do servidor HTTP Molecule.
Concluída esta etapa, finalmente conectaremos todo o microsserviço, pronto para ser executado como um todo.
Se você estiver usando a versão digital deste livro, aconselhamos que você mesmo digite o código ou acesse o
código no repositório GitHub do livro (um link está disponível na próxima seção). Isso o ajudará a evitar possíveis
erros relacionados à cópia e colagem do código.
Machine Translated by Google
XVIII Prefácio
Você pode baixar os arquivos de código de exemplo deste livro no GitHub em https://github.com/
PacktPublishing/Desenvolvimento Orientado a Testes com Java. Se houver uma atualização no
código, ela será atualizada no repositório GitHub.
Também temos outros pacotes de códigos do nosso rico catálogo de livros e vídeos disponíveis em https://
github.com/PacktPublishing/. Confira!
Também fornecemos um arquivo PDF que contém imagens coloridas das capturas de tela e diagramas usados neste livro.
Você pode baixá-lo aqui: https://packt.link/kLcmS.
Convenções usadas
Código no texto: indica palavras de código no texto, nomes de tabelas de banco de dados, nomes de pastas, nomes de
arquivos, extensões de arquivos, nomes de caminhos, URLs fictícios, entrada do usuário e identificadores do Twitter. Aqui
está um exemplo: “Monte o arquivo de imagem de disco WebStorm-10*.dmg baixado como outro disco em seu sistema.”
Quando desejamos chamar sua atenção para uma parte específica de um bloco de código, as linhas ou itens relevantes são
colocados em negrito:
Prefácio XIX
Negrito: indica um novo termo, uma palavra importante ou palavras que você vê na tela. Por exemplo, palavras em
menus ou caixas de diálogo aparecem em negrito. Aqui está um exemplo: “Selecione informações do sistema no painel
de administração”.
Apareça assim.
Entrar em contato
Feedback geral: Se você tiver dúvidas sobre qualquer aspecto deste livro, envie-nos um e-mail para customercare@
packtpub.com e mencione o título do livro no assunto da sua mensagem.
Errata: Embora tenhamos tomado todos os cuidados para garantir a precisão do nosso conteúdo, erros acontecem.
Se você encontrou um erro neste livro, ficaríamos gratos se você nos relatasse isso. Visite www.packtpub.com/support/
errata e preencha o formulário.
Pirataria: Se você encontrar cópias ilegais de nossos trabalhos, sob qualquer forma, na Internet, ficaríamos gratos se
você nos fornecesse o endereço do local ou o nome do site. Entre em contato conosco em copyright@packt.com com
um link para o material.
Se você estiver interessado em se tornar um autor: Se houver um tópico no qual você tenha experiência e estiver
interessado em escrever ou contribuir para um livro, visiteauthors.packtpub.com.
Machine Translated by Google
xx Prefácio
Depois de ler Desenvolvimento Orientado a Testes com Java, adoraríamos ouvir sua opinião! Por favor clique aqui
para ir direto para a página de resenhas deste livro na Amazon e compartilhar seus comentários.
Sua avaliação é importante para nós e para a comunidade de tecnologia e nos ajudará a garantir que estamos entregando
conteúdo de excelente qualidade.
Machine Translated by Google
Prefácio xxi
Você gosta de ler em qualquer lugar, mas não consegue levar seus livros impressos para qualquer lugar?
A compra do seu e-book não é compatível com o dispositivo de sua escolha?
Não se preocupe, agora com cada livro Packt você obtém uma versão em PDF sem DRM desse livro, sem nenhum custo.
Leia em qualquer lugar, em qualquer lugar, em qualquer dispositivo. Pesquise, copie e cole códigos de seus livros técnicos
favoritos diretamente em seu aplicativo.
As vantagens não param por aí, você pode obter acesso exclusivo a descontos, newsletters e ótimo conteúdo gratuito em sua
caixa de entrada diariamente
https://packt.link/free-ebook/978-1-80323-623-0
3. É isso! Enviaremos seu PDF grátis e outros benefícios diretamente para seu e-mail
Machine Translated by Google
Machine Translated by Google
Parte 1:
Como chegamos ao TDD
Na Parte 1, veremos como chegamos ao TDD na indústria de software. Que problemas estamos tentando
resolver com o TDD? Que oportunidades isso cria?
Nos capítulos seguintes, aprenderemos sobre os benefícios que o TDD traz para empresas e desenvolvedores.
Revisaremos os fundamentos de um bom código para fornecer algo a que almejar quando começarmos a escrever
nossos testes. Sabendo que as equipes às vezes hesitam em começar a usar o TDD, examinaremos seis objeções
comuns e como superá-las.
Antes de nos aprofundarmos no que é o desenvolvimento orientado a testes (TDD) e como usá-lo, precisaremos
entender por que precisamos dele. Todo desenvolvedor experiente sabe que código ruim é mais fácil de escrever do
que código bom. Até mesmo um bom código parece piorar com o tempo. Por que?
Neste capítulo, revisaremos as falhas técnicas que dificultam o trabalho com o código-fonte. Consideraremos o
efeito que um código incorreto tem tanto na equipe quanto nos resultados financeiros da empresa. Ao final do
capítulo, teremos uma imagem clara dos antipadrões que precisamos evitar em nosso código.
Meus próprios problemas com códigos incorretos remontam ao meu primeiro programa digno de nota. Este foi um
programa escrito para um concurso escolar, que tinha como objetivo ajudar os corretores de imóveis a ajudar os seus
clientes a encontrar a casa perfeita. Escrito no computador Research Machines 380Z de 8 bits da escola, esta foi a
resposta de 1981 ao Rightmove.
Naqueles dias pré-web, ele existia como um aplicativo de desktop simples com uma interface de usuário baseada
em texto em tela verde. Não precisava lidar com milhões, muito menos bilhões, de usuários. Nem teve que lidar
com milhões de casas. Ele nem tinha uma interface de usuário agradável.
Machine Translated by Google
Como parte do código, eram alguns milhares de linhas de código do Microsoft Disk BASIC 9. Não havia nenhuma
estrutura de código digna de menção, apenas milhares de linhas resplandecentes com números de linhas ímpares e
enfeitadas com variáveis globais. Para adicionar um elemento de desafio ainda maior, o BASIC limitou cada variável
a um nome de duas letras. Isso tornou todos os nomes no código totalmente incompreensíveis. O código-fonte foi
escrito intencionalmente para ter o mínimo de espaços possível para economizar memória. Quando você tinha
apenas 32 KB de RAM para acomodar todo o código do programa, os dados e o sistema operacional, cada byte era importante.
O programa ofereceu apenas recursos básicos ao usuário. A interface do usuário era da época, usando apenas formulários
baseados em texto. Ele antecedeu os sistemas operacionais gráficos em uma década. O programa também teve que
implementar seu próprio sistema de armazenamento de dados, utilizando arquivos em disquetes de 5,25 polegadas. Novamente,
os componentes de banco de dados acessíveis eram o futuro. A principal característica do programa em questão era que os
usuários podiam pesquisar casas dentro de determinadas faixas de preços e conjuntos de recursos. Eles poderiam filtrar por
termos como número de quartos ou faixa de preço.
No entanto, o código em si era realmente uma bagunça. Veja você mesmo – aqui está uma fotografia da listagem original:
Este horror é a listagem original em papel de uma das versões em desenvolvimento. É, como você pode ver, completamente ilegível.
Não é só você. Ninguém seria capaz de lê-lo facilmente. Não posso e escrevi.
Eu iria mais longe e diria que é uma bagunça, minha bagunça, criada por mim, uma tecla de cada vez.
Esse tipo de código é um pesadelo para se trabalhar. Ele falha em nossa definição de bom código. Não é nada fácil ler essa listagem
e entender o que o código deveria fazer. Não é seguro alterar esse código. Se tentássemos, descobriríamos que nunca poderíamos
ter certeza se quebramos algum recurso ou não. Também teríamos que testar novamente manualmente todo o aplicativo. Isso seria
demorado.
Falando em testes, nunca testei exaustivamente esse código. Tudo foi testado manualmente, mesmo sem seguir um plano de teste
formal. Na melhor das hipóteses, eu teria executado alguns testes manuais do caminho feliz. Esses eram o tipo de teste que confirmaria
que era possível adicionar ou excluir uma casa e que algumas pesquisas representativas funcionavam, mas isso era tudo. Nunca
testei todos os caminhos desse código. Eu apenas imaginei que funcionaria.
Se o tratamento dos dados tivesse falhado, eu não saberia o que aconteceu. Eu nunca tentei. Todas as combinações de pesquisa
possíveis funcionaram? Quem sabia? Eu certamente não tinha ideia. Tive ainda menos paciência para passar por todos aqueles
tediosos testes manuais. Funcionou, o suficiente para ganhar uma espécie de prêmio, mas ainda era um código ruim.
No meu caso, foi simplesmente por falta de conhecimento. Eu não sabia escrever um bom código. Mas também existem outras razões
não relacionadas à habilidade. Ninguém jamais se propõe a escrever código incorreto intencionalmente.
Os desenvolvedores fazem o melhor trabalho que podem com as ferramentas disponíveis e com o melhor de sua capacidade naquele momento.
Mesmo com as habilidades certas, vários problemas comuns podem resultar em código incorreto:
• Trabalhar com código legado cuja estrutura impede que novo código seja adicionado de forma limpa
• Adicionar uma solução de curto prazo para uma falha de produção urgente e nunca mais retrabalhá-la
Agora que vimos um exemplo de código difícil de trabalhar e entendemos como ele surgiu, vamos passar à próxima pergunta óbvia:
como podemos reconhecer código incorreto?
Machine Translated by Google
Admitir que nosso código é difícil de trabalhar é uma coisa, mas para superar isso e escrever um bom código,
precisamos entender por que o código é ruim. Vamos identificar os problemas técnicos.
Os nomes são o fator mais crítico para decidir se o código será fácil de trabalhar ou não. Bons nomes dizem claramente ao
leitor o que esperar. Nomes ruins não. As variáveis devem ser nomeadas de acordo com o que contêm. Eles deveriam
responder “por que eu iria querer usar esses dados? O que isso vai me dizer?
Uma variável de string que foi nomeada string está mal nomeada. Tudo o que sabemos é que é uma string.
Isso não nos diz o que há na variável ou por que gostaríamos de usá-la. Se essa string representasse um
sobrenome, simplesmente chamando-a de sobrenome, teríamos ajudado os futuros leitores de nosso
código a entender muito melhor nossas intenções. Eles poderiam ver facilmente que esta variável contém
um sobrenome e não deve ser usada para nenhum outro propósito.
Os nomes de variáveis com duas letras que vimos na listagem da Figura 1.1 representavam uma limitação da
linguagem BASIC. Não foi possível fazer melhor na época, mas como pudemos ver, eles não ajudaram. É muito
mais difícil entender o que sn significa do que sobrenome, se é isso que a variável armazena. Para levar isso
ainda mais longe, se decidirmos manter um sobrenome em uma variável chamada x, tornaremos as coisas
realmente difíceis para os leitores do nosso código. Eles agora têm dois problemas para resolver:
• Eles precisam fazer engenharia reversa do código para descobrir que x é usado para conter um sobrenome
• Eles têm que mapear mentalmente x com o conceito de sobrenome toda vez que o usam
É muito mais fácil quando usamos nomes descritivos para todos os nossos dados, como variáveis locais, parâmetros de
métodos e campos de objetos. Em termos de diretrizes mais gerais, o seguinte guia de estilo do Google é uma boa fonte:
https://google.github.io/styleguide/javaguide.html#s5-naming.
Agora temos uma ideia melhor de como nomear variáveis. Agora, vamos ver como nomear funções, métodos e classes
corretamente.
Os nomes das funções, métodos e classes seguem um padrão semelhante. Em um bom código, os
nomes das funções nos dizem por que devemos chamar essa função. Eles descrevem o que farão por
nós como usuários dessa função. O foco está no resultado – o que terá acontecido quando a função retornar.
Machine Translated by Google
Não descrevemos como essa função é implementada. Isso é importante. Isso nos permite alterar
nossa implementação dessa função posteriormente, se isso for vantajoso, e o nome ainda descreverá
o resultado claramente.
Uma função chamada calculaTotalPrice é clara sobre o que fará por nós. Ele calculará o preço total. Não terá efeitos colaterais
surpreendentes. Não tentará fazer mais nada. Ele fará o que diz que fará. Se abreviarmos esse nome para ctp, ficará muito menos
claro. Se chamarmos isso de func1, então não nos diz absolutamente nada que seja útil.
Nomes ruins nos forçam a fazer engenharia reversa em todas as decisões tomadas sempre que lemos o código. Temos que
examinar o código para tentar descobrir para que ele é usado. Não deveríamos ter que fazer isso. Os nomes devem ser abstrações.
Um bom nome acelerará nossa capacidade de entender o código, condensando uma compreensão geral em poucas palavras.
Você pode pensar no nome da função como um título. O código dentro da função é o corpo do texto. Funciona da mesma forma
que o texto que você está lendo agora tem o título Reconhecendo código incorreto, que nos dá uma ideia geral do conteúdo dos
parágrafos a seguir. Pela leitura do título, esperamos que os parágrafos sejam sobre o reconhecimento de códigos incorretos, nada
mais e nada menos.
Queremos ser capazes de ler rapidamente nosso software através de seus títulos – função, método, classe e nomes de variáveis
– para que possamos nos concentrar no que queremos fazer agora, em vez de reaprender o que foi feito no passado.
Os nomes dos métodos são tratados de forma idêntica aos nomes das funções. Ambos descrevem uma ação a ser tomada.
Da mesma forma, você pode aplicar as mesmas regras para nomes de funções a nomes de métodos.
Novamente, os nomes das classes seguem regras descritivas. Uma classe geralmente representa um único conceito, portanto seu
nome deve descrever esse conceito. Se uma classe representa os dados do perfil do usuário em nosso sistema, então o nome de
classe UserProfile ajudará os leitores de nosso código a entender isso.
Outra dica se aplica a todos os nomes no que diz respeito ao comprimento. O nome deve ser totalmente descritivo, mas seu
comprimento depende de alguns fatores. Podemos escolher nomes mais curtos quando uma das seguintes situações se aplicar:
Vejamos um exemplo de código para cada caso para deixar isso claro.
Machine Translated by Google
O código a seguir calcula o total de uma lista de valores, usando um nome curto de variável, total:
total de retorno;
}
Isso funciona bem porque fica claro que total representa o total de todos os valores. Não precisamos
mais de um nome que tenha mais o contexto em torno dele no código. Talvez um exemplo ainda
melhor esteja na variável do loop v . Tem um escopo de uma linha e, dentro desse escopo, é bastante
claro que v representa o valor atual dentro do loop. Poderíamos usar um nome mais longo, como currentValue .
No entanto, isso acrescenta alguma clareza? Na verdade.
A razão pela qual podemos escolher um nome tão curto é que a classe GraphicsContext já contém a
maior parte da descrição. Se esta fosse uma classe de propósito mais geral, como String, por exemplo,
então esta técnica de nome abreviado seria inútil.
Neste exemplo de código final, estamos usando o nome abreviado do método draw():
O nome da classe aqui é altamente descritivo. O nome da classe ProfileImage que usamos em nosso
sistema é comumente usado para descrever o avatar ou fotografia que aparece na página de perfil de um usuário.
O método draw() é responsável por gravar os dados da imagem em um objeto WebResponse . Poderíamos
escolher um nome de método mais longo, como drawProfileImage(), mas que simplesmente repita
informações que já foram esclarecidas com o nome da classe. Detalhes como esse são o que dão ao Java sua
Machine Translated by Google
reputação de ser prolixo, o que considero injusto; muitas vezes somos nós, programadores Java, que somos
prolixos, e não o próprio Java.
Vimos como nomear as coisas corretamente torna nosso código mais fácil de entender. Vamos dar uma olhada no próximo
grande problema que vemos em códigos incorretos – o uso de construções que aumentam a probabilidade de erros lógicos.
Outro sinal revelador de código incorreto é que ele usa construções e designs propensos a erros. Sempre
existem várias maneiras de fazer a mesma coisa no código. Alguns deles oferecem mais espaço para introduzir
erros do que outros. Portanto, faz sentido escolher formas de codificação que evitem erros ativamente.
Vamos comparar duas versões diferentes de uma função para calcular um valor total e analisar onde os erros
podem ocorrer:
total de retorno;
}
A listagem anterior é um método simples que pegará uma lista de números inteiros e retornará seu total.
É o tipo de código que existe desde o Java 1.0.2. Funciona, mas é propenso a erros. Para que esse
código esteja correto, precisamos acertar várias coisas:
• Certificando-se de que o total seja inicializado como 0 e não com algum outro valor
Programadores experientes tendem a acertar tudo isso na primeira vez. O que quero dizer é que existe uma possibilidade
de errar alguma ou todas essas coisas. Já vi erros cometidos onde <= foi usado em vez de < e o
código falha com uma exceção ArrayIndexOutOfBounds como resultado. Outro erro fácil é usar =
na linha que soma o valor total em vez de +=. Isto tem o efeito de retornar apenas o último valor,
não o total. Eu até cometi esse erro por puro erro de digitação – honestamente pensei que tinha
digitado a coisa certa, mas estava digitando rapidamente e não o fiz.
Machine Translated by Google
É claramente muito melhor para nós evitarmos totalmente este tipo de erros. Se um erro não puder acontecer, então
isso não acontecerá. Este é um processo que chamo de eliminação de erros. É uma prática fundamental de código limpo.
Para ver como poderíamos fazer isso em nosso exemplo anterior, vejamos o seguinte código:
Este código faz a mesma coisa, mas é inerentemente mais seguro. Não temos uma variável total , portanto não podemos inicializá
-la incorretamente, nem podemos esquecer de adicionar valores a ela. Não temos loop e, portanto, nenhuma variável de índice
de loop. Não podemos usar a comparação errada para o final do loop e, portanto, não podemos obter um ArrayIndexOutOfBounds
exceção. Simplesmente há muito menos coisas que podem dar errado nesta implementação do código. Geralmente
também torna o código mais claro para leitura. Isso, por sua vez, ajuda na integração de novos desenvolvedores,
revisões de código, adição de novos recursos e programação em pares.
Sempre que tivermos a opção de usar código com menos partes que possam dar errado, devemos escolher essa
abordagem. Podemos tornar a vida mais fácil para nós mesmos e para nossos colegas, optando por manter nosso código
o mais simples e livre de erros possível. Podemos usar construções mais robustas para dar aos bugs menos lugares para se esconder.
Vale ressaltar que ambas as versões do código possuem bug de overflow de inteiro. Se somarmos números inteiros
cujo total está além do intervalo permitido de -2147483648 a 2147483647, o código produzirá o resultado errado. A
questão permanece, porém: a versão posterior tem menos lugares onde as coisas podem dar errado. Estruturalmente,
é um código mais simples.
Agora que vimos como evitar os tipos de erros típicos de códigos incorretos, vamos passar para outras áreas
problemáticas: acoplamento e coesão.
Acoplamento e coesão
Se tivermos várias classes Java, o acoplamento descreve o relacionamento entre essas classes, enquanto a coesão
descreve os relacionamentos entre os métodos dentro de cada uma.
Nossos projetos de software se tornam mais fáceis de trabalhar quando acertamos as quantidades de
acoplamento e coesão . Aprenderemos técnicas para nos ajudar a fazer isso no Capítulo 7, Driving Design – TDD e SOLID.
Por enquanto, vamos entender os problemas que enfrentaremos quando errarmos nisso, começando pelo problema da
baixa coesão.
A baixa coesão descreve um código que contém muitas ideias diferentes, todas agrupadas em um único lugar.
O diagrama de classes UML a seguir mostra um exemplo de classe com baixa coesão entre seus métodos:
Machine Translated by Google
O código desta classe tenta combinar muitas responsabilidades. Eles não estão todos obviamente relacionados –
estamos gravando em um banco de dados, enviando e-mails de boas-vindas e renderizando páginas da web. Essa
grande variedade de responsabilidades torna nossa classe mais difícil de entender e mais difícil de mudar. Considere
os diferentes motivos pelos quais podemos precisar alterar esta classe:
Existem muitos motivos pelos quais precisaríamos alterar o código nesta classe. É sempre melhor dar um foco mais preciso às
aulas, para que haja menos motivos para alterá-las. Idealmente, qualquer trecho de código deve ter apenas um motivo para ser
alterado.
Compreender o código com baixa coesão é difícil. Somos forçados a compreender muitas ideias diferentes ao
mesmo tempo. Internamente, o código está muito interligado. Mudar um método muitas vezes força uma
mudança em outros por causa disso. Usar a classe é difícil, pois precisamos construí-la com todas as suas dependências.
Em nosso exemplo, temos uma mistura de mecanismos de modelagem, um banco de dados e código para criar uma página web.
Isso também torna a classe muito difícil de testar. Precisamos configurar todas essas coisas antes de podermos executar métodos
de teste nessa classe. A reutilização é limitada com uma classe como esta. A classe está fortemente ligada à combinação de
recursos incluídos nela.
Machine Translated by Google
O alto acoplamento descreve onde uma classe precisa se conectar a várias outras antes de poder ser usada.
Isso dificulta o uso isolado. Precisamos que essas classes de suporte estejam configuradas e funcionando corretamente
antes de podermos usar nossa classe. Pela mesma razão, não podemos compreender completamente essa classe sem
compreender as muitas interações que ela possui. Como exemplo, o diagrama de classes UML a seguir mostra classes
com alto grau de acoplamento entre si:
Neste exemplo fictício de sistema de rastreamento de vendas, várias classes precisam interagir umas com as outras . A
classe User no meio se une a quatro outras classes: Inventory, EmailService, SalesAppointment e SalesReport. Isso
torna mais difícil usar e testar do que uma classe que é associada a menos outras classes. O acoplamento aqui é muito
alto? Talvez não, mas podemos imaginar outros designs que o reduziriam. O principal é estar atento ao grau de
acoplamento que as classes possuem em nossos projetos. Assim que identificarmos classes com muitas conexões com
outras, sabemos que teremos problemas para compreendê-las, mantê-las e testá-las.
Vimos como os elementos técnicos de alto acoplamento e baixa coesão tornam difícil trabalhar com nosso código, mas
também há um aspecto social no código ruim. Vamos considerar o efeito que um código incorreto tem na equipe de
desenvolvimento.
Quando você está codificando sozinho, isso não importa tanto. Código incorreto apenas irá atrasá-lo e, às vezes, parecer
um pouco desmoralizante. Não afeta mais ninguém. No entanto, a maioria dos profissionais codifica em equipes de
desenvolvimento, o que é um jogo totalmente diferente. Código incorreto realmente retarda a equipe.
• https://dl.acm.org/doi/abs/10.1145/3194164.3194178
• https://www.sciencedirect.com/science/article/abs/pii/
S0164121219301335
O primeiro estudo mostra que os desenvolvedores perdem até 23% do seu tempo com códigos ruins. O
segundo estudo mostra que em 25% dos casos de trabalho com código incorreto, os desenvolvedores são
forçados a aumentar ainda mais a quantidade de código incorreto. Nestes dois estudos, é utilizado o termo
dívida técnica , em vez de se referir a código incorreto. Há uma diferença de intenção entre os dois termos.
Dívida técnica é um código enviado com deficiências técnicas conhecidas para cumprir um prazo. Ele é
rastreado e gerenciado com a intenção de ser posteriormente substituído. Código incorreto pode ter os mesmos
defeitos, mas carece da qualidade redentora da intencionalidade.
É muito fácil fazer check-in de código que foi fácil de escrever, mas será difícil de ler. Quando faço isso, coloco efetivamente
um imposto sobre a equipe. O próximo desenvolvedor a fazer minhas alterações terá que descobrir o que eles precisam
fazer e meu código incorreto terá tornado isso muito mais difícil.
Todos nós já estivemos lá. Começamos um trabalho, baixamos o código mais recente e depois ficamos olhando para
nossas telas por muito tempo. Vemos nomes de variáveis que não fazem sentido, misturados com códigos emaranhados
que realmente não se explicam muito bem. É frustrante para nós pessoalmente, mas tem um custo real no negócio de
programação. Cada minuto que gastamos sem entender o código é um minuto em que dinheiro é gasto para não
conseguirmos nada. Não é o que sonhamos quando nos inscrevemos para ser desenvolvedor.
Código incorreto atrapalha todos os futuros desenvolvedores que precisam ler o código, até mesmo nós, os autores
originais. Esquecemos o que queríamos dizer anteriormente. Código ruim significa mais tempo gasto pelos desenvolvedores
corrigindo erros, em vez de agregar valor. Isso significa que se perde mais tempo corrigindo bugs na produção que
deveriam ser facilmente evitáveis.
Pior ainda, esse problema se agrava. É como os juros de um empréstimo bancário. Se deixarmos o código incorreto, o
próximo recurso envolverá a adição de soluções alternativas para o código incorreto. Você pode ver condicionais extras
aparecerem, dando ao código ainda mais caminhos de execução e criando mais locais para os bugs se esconderem. Os
recursos futuros são baseados no código incorreto original e em todas as suas soluções alternativas. Ele cria código onde
a maior parte do que lemos é simplesmente uma solução para o que nunca funcionou bem em primeiro lugar.
Código desse tipo esgota a motivação dos desenvolvedores. A equipe começa a gastar mais tempo resolvendo problemas
do que agregando valor ao código. Nada disso é divertido para o desenvolvedor típico. Não é divertido para ninguém da
equipe.
Os gerentes de projeto perdem o controle do status do projeto. As partes interessadas perdem a confiança na capacidade de entrega da
equipe. Superação de custos. Os prazos escorregam. Os recursos são cortados silenciosamente, apenas para reduzir um pouco a folga no
Machine Translated by Google
agendar. A integração de novos desenvolvedores torna-se dolorosa, ao ponto da estranheza, sempre que eles
veem o código horrível.
Código incorreto deixa toda a equipe incapaz de atingir o nível de desempenho que é capaz. Isso, por sua vez, não contribui para
uma equipe de desenvolvimento feliz. Além dos desenvolvedores insatisfeitos, também impacta negativamente os resultados dos
negócios. Vamos entender essas consequências.
Nossos pobres usuários acabam pagando por software que não funciona, ou pelo menos que não funciona corretamente. Códigos
incorretos podem atrapalhar o dia de um usuário de muitas maneiras, seja como resultado de perda de dados, interfaces de usuário
que não respondem ou qualquer tipo de falha intermitente. Cada um deles pode ser causado por algo tão trivial como definir uma
variável no momento errado ou um erro off-by-one em algum lugar condicional.
Os usuários não veem nada disso nem as milhares de linhas de código que acertamos. Eles apenas veem o pagamento perdido, o
documento perdido que levou 2 horas para ser digitado ou aquele fantástico negócio de última chance que simplesmente nunca
aconteceu. Os usuários têm pouca paciência para coisas assim. Defeitos desse tipo podem facilmente nos fazer perder um cliente
valioso.
Se tivermos sorte, os usuários preencherão um relatório de bug. Se tivermos muita sorte, eles nos informarão o que estavam
fazendo naquele momento e nos fornecerão os passos corretos para reproduzir a falha. Mas a maioria dos usuários simplesmente
clicará em excluir em nosso aplicativo. Eles cancelarão assinaturas futuras e pedirão reembolso. Eles irão avaliar sites e deixarão o
mundo saber o quão inúteis nosso aplicativo e nossa empresa são.
Neste ponto, não se trata apenas de um código incorreto; é uma responsabilidade comercial. As falhas e erros humanos honestos
em nossa base de código foram esquecidos há muito tempo. Em vez disso, éramos apenas uma empresa concorrente que ia e
vinha numa onda de negatividade.
A diminuição da receita leva à diminuição da participação de mercado, à redução do Net Promoter Score®™
(NPS), aos acionistas decepcionados e a todas as outras coisas que fazem seu C-suite perder o sono à noite.
Nosso código incorreto se tornou um problema no nível empresarial.
Isso não é hipotético. Houve vários incidentes em que falhas de software custaram caro aos negócios.
Violações de segurança para Equifax, Target e até mesmo para o site Ashley Madison resultaram em perdas. O foguete Ariane
resultou na perda da espaçonave e da carga do satélite, num custo total de bilhões de dólares! Mesmo incidentes menores que
resultem em tempo de inatividade dos sistemas de comércio eletrónico podem em breve ter custos crescentes, enquanto a
confiança do consumidor cai.
Em cada caso, as falhas podem ter sido pequenos erros em comparativamente poucas linhas de código. Certamente, eles terão
sido evitáveis de alguma forma. Sabemos que os humanos cometem erros e que todo o software é construído por humanos, mas
um pouco de ajuda extra pode ter sido tudo o que seria necessário para impedir que estes desastres se desenrolassem.
Na figura anterior, o custo do reparo de um defeito aumenta quanto mais tarde ele for encontrado:
A maneira mais barata e rápida de descobrir um defeito é escrever um teste para um recurso antes de escrevermos o código de
produção. Se escrevermos o código de produção que esperamos que faça o teste passar, mas o teste falhar, sabemos que há um
Se escrevermos o código de produção para um recurso e depois escrevermos um teste, poderemos encontrar defeitos em nosso
código de produção. Isso acontece um pouco mais tarde no ciclo de desenvolvimento. Teremos perdido um pouco mais de tempo
Muitas equipes incluem engenheiros de garantia de qualidade (QA) . Depois que o código for escrito por um desenvolvedor,
o engenheiro de controle de qualidade testará o código manualmente. Se um defeito for encontrado aqui, isso significa que já
passou um tempo significativo desde que o desenvolvedor escreveu o código pela primeira vez. O retrabalho terá que ser feito.
Isso é o pior que pode acontecer. O código foi enviado para produção e os usuários finais estão usando-o.
Um usuário final encontra um bug. O bug deve ser relatado, triado, uma correção agendada para desenvolvimento, depois testada
novamente pelo controle de qualidade e reimplantada na produção. Este é o caminho mais lento e caro para descobrir um defeito.
Machine Translated by Google
Quanto mais cedo encontrarmos a falha, menos tempo e dinheiro teremos para gastá-la. O ideal é ter um teste com
falha antes mesmo de escrevermos uma linha de código. Essa abordagem também nos ajuda a projetar nosso
código. Quanto mais tarde deixarmos para encontrar um erro, mais problemas isso causará para todos.
Vimos como códigos de baixa qualidade geram defeitos e são ruins para os negócios. Quanto mais cedo detectarmos falhas,
melhor será para nós. Deixar defeitos no código de produção é difícil e caro de corrigir e afeta negativamente a reputação de
nossa empresa.
Resumo
Agora podemos reconhecer códigos ruins por seus sinais técnicos e avaliar os problemas que eles causam tanto para as equipes
de desenvolvimento quanto para os resultados de negócios.
O que precisamos é de uma técnica que nos ajude a evitar esses problemas. No próximo capítulo, veremos como o TDD nos
ajuda a fornecer código limpo e correto, que é um verdadeiro ativo de negócios.
Perguntas e respostas
1. Não é suficiente ter um código funcional?
Infelizmente, não. O código que atende às necessidades do usuário é uma etapa básica do software profissional.
Também precisamos de um código que sabemos que funciona e que a equipe possa entender e modificar facilmente.
2. Os usuários não veem o código. Por que isso é importante para eles?
Isto é verdade. No entanto, os usuários esperam que tudo funcione de maneira confiável e que nosso software seja
atualizado e melhorado continuamente. Isso só é possível quando os desenvolvedores conseguem trabalhar com
segurança com o código existente.
É muito mais difícil escrever um bom código, infelizmente. Um bom código faz mais do que simplesmente funcionar
corretamente. Também deve ser fácil de ler, fácil de alterar e seguro para nossos colegas trabalharem.
É por isso que técnicas como o TDD têm um papel importante a desempenhar. Precisamos de toda a ajuda possível para
escrever um código limpo que ajude nossos colegas.
Leitura adicional
• Mais sobre a perda do foguete Ariane: https://www.esa.int/Newsroom/Press_
Lançamentos/Ariane_501_-_Presentation_of_Inquiry_Board_report
Machine Translated by Google
2
Usando TDD para criar um bom código
Vimos que códigos ruins são más notícias: ruins para os negócios, ruins para os usuários e ruins para os desenvolvedores. O
desenvolvimento orientado a testes (TDD) é uma prática central de engenharia de software que nos ajuda a manter códigos
incorretos fora de nossos sistemas.
O objetivo deste capítulo é aprender os detalhes de como o TDD nos ajuda a criar código correto e bem projetado, e como ele nos
ajuda a mantê-lo assim. Ao final, entenderemos os princípios básicos por trás de um bom código e como o TDD nos ajuda a criá-lo. É
importante entendermos por que o TDD funciona para nos motivar e para que tenhamos uma resposta para dar aos colegas sobre
por que recomendamos que eles também o utilizem.
Cada linha de código-fonte envolve pelo menos uma dessas decisões. Isso é uma decisão enorme que temos que fazer.
Machine Translated by Google
Você notará que não mencionamos o TDD até agora. Como veremos, o TDD não projeta seu código para você. Isso
não elimina a sensibilidade essencial da engenharia e a contribuição criativa necessária para transformar requisitos
em código. Para ser honesto, estou grato por isso – é a parte que eu gosto.
No entanto, isso causa muitas falhas precoces no TDD, o que é digno de nota. Esperar implementar o processo TDD
e obter código de boa qualidade sem a sua própria contribuição de design simplesmente não funcionará.
O TDD, como veremos, é uma ferramenta que permite obter feedback rápido sobre essas decisões de design. Você
pode mudar de ideia e se adaptar enquanto o código ainda é barato e rápido de mudar, mas eles ainda são seus
decisões de design que estão acontecendo.
Um bom código, para mim, tem tudo a ver com legibilidade. Eu otimizo para maior clareza. Quero ser gentil comigo
mesmo no futuro e com meus colegas sofredores, criando um código que seja claro e seguro para trabalhar. Quero
criar um código claro e simples, livre de armadilhas ocultas.
Embora haja uma grande variedade de conselhos sobre o que constitui um bom código, o básico é simples:
• Diga o que você quer dizer, seja sincero no que você diz
Vale a pena uma rápida revisão do que quero dizer com essas coisas.
Diga o que você quer dizer, fale sério o que você diz
Aqui está uma experiência interessante. Pegue um pedaço de código-fonte (em qualquer linguagem) e retire tudo que
não faz parte da especificação da linguagem, então veja se você consegue descobrir o que ele faz. Para que as coisas
realmente se destaquem, substituiremos todos os nomes de métodos e identificadores de variáveis pelo símbolo ???.
retornar ???;
Alguma idéia do que esse código faz? Eu também não. Eu não tenho a menor ideia.
Machine Translated by Google
Posso dizer pelo seu formato que é algum tipo de método de avaliação que passa algo e retorna verdadeiro/falso.
Talvez implemente um limite ou limite. Ele usa uma estrutura de retorno multipath, onde verificamos algo e retornamos
uma resposta assim que sabemos qual é essa resposta.
Embora a forma do código e a sintaxe nos digam algo, não nos dizem muito. Definitivamente não é suficiente. Quase
todas as informações que compartilhamos sobre o que nosso código faz são resultado dos identificadores de
linguagem natural que escolhemos. Os nomes são absolutamente vitais para um bom código. Eles estão além de importantes.
Eles são tudo. Eles podem revelar intenções, explicar resultados e descrever por que um dado é importante para nós,
mas não poderão fazer nada disso se fizermos um mau trabalho ao escolher nossos nomes.
Eu uso duas diretrizes para nomes, uma para nomear código ativo – métodos e funções – e outra para variáveis:
• Método – Diga o que faz. Qual é o resultado? Por que eu ligaria para isso?
Um erro comum na nomenclatura de métodos é descrever como ele funciona internamente, em vez de descrever
qual é o resultado. Um método chamado addTodoItemToItemQueue está nos comprometendo com uma
implementação específica de um método com o qual realmente não nos importamos. Ou isso ou é desinformação.
Podemos melhorar o nome chamando-o de add(Todo item). Este nome nos diz exatamente por que devemos
chamar esse método. Isso nos deixa livres para revisar como ele é codificado posteriormente.
O erro clássico com nomes de variáveis é dizer do que elas são feitas. Por exemplo, o nome da
variável String string não ajuda ninguém, enquanto String firstName me diz claramente que esta
variável é o primeiro nome de alguém. Isso me diz por que eu gostaria de ler ou escrever essa variável.
Talvez o mais importante seja que nos diz o que não devemos escrever nessa variável. Ter uma variável servindo a
vários propósitos no mesmo escopo é uma verdadeira dor de cabeça. Estive lá, fiz isso, nunca mais voltei.
Acontece que o código é uma narrativa pura e simples. Contamos a história de qual problema estamos resolvendo
e como decidimos resolvê-lo para programadores humanos. Podemos colocar qualquer código antigo em um
compilador e o computador o fará funcionar, mas devemos tomar mais cuidado se quisermos que os humanos
entendam nosso trabalho.
A maneira como penso sobre a abstração é a mesma que penso sobre contratar um eletricista para minha casa.
Sei que meu aquecedor elétrico de água precisa ser consertado, mas não quero saber como. Eu não
quero aprender como fazer isso. Não quero ter que descobrir quais ferramentas são necessárias e
comprá-las. Não quero ter nada a ver com isso, além de pedir que seja feito quando eu precisar. Então, eu vou
Machine Translated by Google
chame o eletricista e peça para ele fazer isso. Fico mais do que feliz em pagar por um bom trabalho, desde que não
precise fazer isso sozinho.
Isto é o que significa abstração. O eletricista abstrai o trabalho de consertar meu aquecedor de água. Coisas complexas são
feitas em resposta aos meus pedidos simples.
Cada vez que você torna algum tipo de detalhe menos importante, você o abstrai. Um método possui uma
assinatura simples, mas o código dentro dele pode ser complexo. Esta é uma abstração de um algoritmo. Uma
variável local pode ser declarada como tipo String. Esta é uma abstração do gerenciamento de memória de cada
caractere de texto e da codificação de caracteres. Um microsserviço que enviará vouchers de desconto aos
nossos principais clientes que não visitam o site há algum tempo é uma abstração de um processo de negócios. Abstração
está em toda parte na programação, em todos os principais paradigmas – programação orientada a objetos (OOP),
processual e funcional.
A ideia de dividir o software em componentes, cada um dos quais cuida de algo para nós, é um grande impulsionador da
qualidade. Centralizamos as decisões, o que significa que não cometemos erros em códigos duplicados. Podemos testar um
componente completamente isoladamente. Projetamos problemas causados por código difícil de escrever apenas escrevendo-
o uma vez e tendo uma interface fácil de usar.
Sempre há muitas maneiras de escrever um trecho de código. Alguns deles usam recursos complicados ou circulam pelas
casas; eles usam cadeias complicadas de ações para fazer algo simples. Todas as versões do código obtêm o mesmo
resultado, mas algumas o fazem de maneira mais complicada por acidente.
Meu objetivo com o código é contar à primeira vista a história de qual problema estou resolvendo, deixando os detalhes de
como estou resolvendo para uma análise mais detalhada. Isso é bem diferente de como aprendi a codificar originalmente. Opto
por enfatizar o domínio sobre o mecanismo. O domínio aqui significa usar a mesma linguagem do usuário, por exemplo,
expressando o problema em termos comerciais, não apenas na sintaxe bruta do código de computador. Se estou escrevendo
um sistema bancário, quero ver o dinheiro, os livros contábeis e as transações em primeiro plano. A história que o código conta
deve ser a do setor bancário.
Detalhes de implementação, como filas de mensagens e bancos de dados, são importantes, mas apenas na medida em que
descrevem como estamos resolvendo o problema hoje. Eles podem precisar ser alterados mais tarde. Quer elas mudem ou
não, ainda queremos que a história principal seja sobre transações que entram em uma conta e não sobre filas de mensagens
conversando com serviços REST.
À medida que nosso código fica melhor em contar a história do problema que estamos resolvendo, fica mais fácil escrever
componentes de substituição. Trocar um banco de dados por um produto de outro fornecedor é simplificado porque sabemos
exatamente a que finalidade ele está servindo em nosso sistema.
Machine Translated by Google
Isso é o que queremos dizer com ocultar detalhes. Em algum nível, é importante ver como conectamos
o banco de dados, mas somente depois de vermos por que precisávamos de um.
Para dar um exemplo concreto, aqui está um trecho de código semelhante a algum código que encontrei em um
sistema de produção:
if (b == nulo) {
resultado = falso;
}
senão if ( b.equals(Boolean.TRUE)) {
resultado = verdadeiro;
}
senão if ( b.equals(Boolean.FALSE)) {
resultado = falso;
}
outro {
resultado = falso;
}
resultado de retorno;
}
Você pode ver o problema aqui. Sim, há necessidade de um método como este. É um mecanismo de baixo nível que
converte um objeto Java verdadeiro/falso em seu tipo primitivo equivalente e faz isso com segurança. Ele cobre todos os
casos extremos relacionados a uma entrada de valor nulo , bem como valores verdadeiros/falso válidos.
No entanto, tem problemas. Este código está confuso. É desnecessariamente difícil de ler e testar. Possui alta
complexidade ciclomática (CYC). CYC é uma medida objetiva de quão complexo é um trecho de código, com
base no número de caminhos de execução independentes possíveis em uma seção de código.
O código anterior é desnecessariamente detalhado e complicado demais. Tenho quase certeza de que ele também possui um
caminho de código morto – ou seja, um caminho contendo código inacessível – nesse outro ponto final.
Observando a lógica necessária, existem apenas três condições de entrada interessantes: nula, verdadeira
e falsa. Certamente não são necessárias todas aquelas cadeias else/if para decodificar isso. Depois de
eliminar a conversão de nulo para falso, você realmente só precisa inspecionar um valor antes de decidir
completamente o que retornar.
Machine Translated by Google
Este código faz a mesma coisa com muito menos barulho. Não possui o mesmo nível de complexidade acidental do
código anterior. Lê melhor. É mais fácil testar com menos caminhos que necessitam de testes. Possui uma melhor
complexidade ciclomática, o que significa menos lugares para os insetos se esconderem. Conta uma história melhor
sobre por que o método existe. Para ser totalmente honesto, posso até refatorar esse método incorporando-o. Não
tenho certeza se o método adiciona alguma explicação extra válida à implementação.
Este método foi um exemplo simples. Imagine ver isso ampliado para milhares de linhas de código copiado e
colado e ligeiramente alterado. Você pode ver por que a complexidade acidental é assassina. Esse lixo se acumula
com o tempo e cresce exponencialmente. Tudo se torna mais difícil de ler e mais difícil de mudar com segurança.
Sim, eu vi isso. Nunca vou deixar de ficar triste com isso quando isso acontecer. Nós podemos fazer melhor que isso. Como
engenheiros de software profissionais, realmente deveríamos.
Esta seção foi um tour relâmpago pelos bons fundamentos do design. Eles se aplicam a todos os estilos de
programação. No entanto, se podemos fazer as coisas certas, também podemos fazer as coisas erradas. Na
próxima seção, veremos como os testes TDD podem nos ajudar a evitar projetos ruins.
O primeiro grande benefício do TDD é que ele nos obriga a pensar no design de um componente. Fazemos isso
antes de pensar em como implementá-lo. Ao fazer as coisas nesta ordem, é muito menos provável que caiamos
num projeto ruim por engano.
A maneira como consideramos o design primeiro é pensar nas interfaces públicas de um componente. Pensamos
em como esse componente será usado e como será chamado. Ainda não consideramos como faremos com que
qualquer implementação realmente funcione. Este é um pensamento de fora para dentro. Consideramos o uso do
código de chamadores externos antes de considerarmos qualquer implementação interna.
Esta é uma abordagem bastante diferente para muitos de nós. Normalmente, quando precisamos de código para fazer alguma
coisa, começamos escrevendo a implementação. Depois disso, iremos distribuir tudo o que for necessário nas assinaturas
dos métodos, sem pensar no site da chamada. Este é um pensamento de dentro para fora. Funciona, é claro, mas geralmente
leva a códigos de chamada complexos. Isso nos prende a detalhes de implementação que simplesmente não são importantes.
Machine Translated by Google
Pensar de fora para dentro significa que podemos sonhar com o componente perfeito para seus usuários. Em seguida,
flexionaremos a implementação para funcionar com o código desejado no site da chamada. Em última análise, isto é
muito mais importante do que a implementação. Isto é, obviamente, uma abstração sendo usada na prática.
• É fácil de configurar?
Você pode ver que, fazendo o tipo certo de perguntas, obteremos o tipo certo de resultados.
Ao escrever os testes primeiro, cobrimos todas essas questões. Decidimos antecipadamente como vamos configurar nosso
componente, talvez decidindo sobre uma assinatura clara de construtor para um objeto. Nós decidimos como vamos fazer
o código de chamada e qual será o site de chamada. Nós decidimos como consumiremos quaisquer resultados retornados
ou qual será o efeito nos componentes colaboradores.
Este é o coração do design de software. O TDD não faz isso por nós, nem nos obriga a fazer um bom
trabalho. Ainda poderíamos encontrar respostas terríveis para todas essas perguntas e simplesmente
escrever um teste para fixar essas respostas ruins. Já vi isso acontecer em diversas ocasiões em código real também.
O TDD oferece essa oportunidade inicial para refletir sobre nossas decisões. Estamos literalmente escrevendo o primeiro
exemplo de um site de chamada executável e funcional para nosso código antes mesmo de pensar em como ele funcionará.
Estamos totalmente focados em como esse novo componente se encaixará no cenário geral.
O teste em si fornece feedback imediato sobre o desempenho de nossas decisões. Dá três sinais reveladores de que
poderíamos e deveríamos melhorar. Guardaremos os detalhes para um capítulo posterior, mas o próprio código de teste
mostra claramente quando seu componente é difícil de configurar, difícil de chamar ou suas saídas são difíceis de trabalhar.
Existem três momentos em que você pode escolher escrever testes: antes do código, depois do código ou nunca.
Obviamente, nunca escrever nenhum teste nos leva de volta à idade das trevas do desenvolvimento. Estamos improvisando.
Escrevemos o código presumindo que ele funcionará e depois deixamos tudo para um estágio de teste manual. Se tivermos
sorte, descobriremos erros funcionais nesta fase, antes dos nossos clientes.
Escrever testes logo após concluirmos um pequeno pedaço de código é uma opção muito melhor. Recebemos feedback
muito mais rápido. Nosso código não é necessariamente melhor, porque escrevemos com a mesma mentalidade que
fazemos sem a implementação de testes. Os mesmos tipos de erros funcionais estarão presentes. A boa notícia é que
escreveremos testes para descobri-los.
Machine Translated by Google
Esta é uma grande melhoria, mas ainda não é o padrão ouro, pois leva a alguns problemas sutis:
• Testes ausentes
A falta de testes acontece por causa da natureza humana. Quando estamos ocupados escrevendo código, temos muitas ideias
em nossas cabeças ao mesmo tempo. Nós nos concentramos em detalhes específicos em detrimento de outros. Sempre acho
que prossigo mentalmente um pouco rápido demais depois de uma linha de código. Eu apenas presumo que tudo ficará bem.
Infelizmente, quando vou escrever meus testes, isso significa que esqueci alguns pontos-chave.
Provavelmente terei começado rapidamente com a verificação de > 18 anos , depois segui mentalmente e
lembrei que a idade poderia ser nula. Terei adicionado a cláusula And para verificar se é ou não. Isso faz
sentido. Minha experiência me diz que esse trecho específico de código precisa fazer mais do que uma
verificação básica e robusta.
Quando eu escrever meu teste, lembrarei de escrever um teste para o que acontece quando eu passo nulo, pois isso está fresco em
minha mente. Então, escreverei outro teste para o que acontece com uma idade mais avançada, digamos 21 anos. Mais uma vez, ótimo.
Provavelmente, eu esquecerei de escrever um teste para o caso extremo de uma idade de 18 anos . Isso
é muito importante aqui, mas minha mente já mudou desse detalhe. Bastará uma mensagem do Slack de
um colega sobre o que há para o almoço, e provavelmente esquecerei tudo sobre esse teste e começarei
a codificar o próximo método.
O código anterior contém um bug sutil. Supõe-se que retorne verdadeiro para qualquer idade com 18 anos ou
mais. Isso não acontece. Ele retorna verdadeiro apenas para 19 anos ou mais. O símbolo maior que deveria ser
um símbolo maior ou igual, mas perdi esse detalhe.
Não apenas perdi a nuance do código, mas também perdi um teste vital. Escrevi dois testes importantes , mas
precisava de três.
Como escrevi os outros testes, não recebo nenhum aviso sobre isso. Você não recebe um teste reprovado que
não tenha escrito.
Podemos evitar isso escrevendo um teste com falha para cada parte do código e, em seguida, adicionando
apenas o código suficiente para fazer o teste passar. Esse fluxo de trabalho teria mais probabilidade de nos levar
a pensar nos quatro testes necessários para eliminar o tratamento nulo e os três casos-limite relacionados à
idade. É claro que não pode garantir isso, mas pode conduzir ao tipo certo de pensamento.
Machine Translated by Google
Abstrações com vazamento são um problema diferente. É aqui que nos concentramos tanto no interior do método que
esquecemos de pensar no local de chamada dos nossos sonhos. Nós apenas reproduzimos o que for mais fácil de codificar.
Poderíamos estar escrevendo uma interface onde armazenamos objetos UserProfile . Podemos prosseguir com o código
primeiro, escolher uma biblioteca JDBC de nossa preferência, codificar o método e descobrir que ele precisa de uma
conexão com o banco de dados.
interface StoredUserProfiles {
Carga do UserProfile (conexão de conexão, int userId);
À primeira vista, não há nada de errado com isso. No entanto, observe o primeiro parâmetro: é o objeto
Connection específico do JDBC . Bloqueamos nossa interface para usar JDBC. Ou pelo menos , ter que
fornecer alguma coisa relacionada ao JDBC como primeiro parâmetro. Nós nem queríamos fazer isso.
Simplesmente não tínhamos pensado nisso completamente.
Se pensarmos na abstração ideal, ela deve carregar o objeto UserProfile correspondente para o
userId fornecido. Não deve saber como é armazenado. O parâmetro Connection específico do
JDBC não deveria estar lá.
Se pensarmos de fora para dentro e considerarmos o design antes da implementação, é menos provável que enveredemos
por este caminho.
Abstrações com vazamento como essa criam complexidade acidental. Eles tornam o código mais difícil de entender,
forçando futuros leitores a se perguntarem por que insistimos no uso de JDBC quando nunca pretendíamos fazê-lo.
Nós apenas esquecemos de projetá-lo.
Escrever testes primeiro ajuda a evitar isso. Isso nos leva a pensar nas abstrações ideais como um primeiro passo para que
possamos escrever o teste para elas.
Assim que tivermos esse teste codificado, teremos decidido como o código será usado. Então, podemos descobrir como
implementar isso sem que nenhum detalhe indesejado vaze.
As técnicas explicadas anteriormente são simples, mas cobrem a maior parte dos princípios básicos de um bom design. Use
nomes claros. Use lógica simples. Use a abstração para ocultar detalhes de implementação, de modo que enfatizemos qual
problema estamos resolvendo, e não como o estamos resolvendo. Na próxima seção, vamos revisar o benefício mais óbvio
do TDD: prevenir falhas em nossa lógica.
Não posso discordar aqui – isso é muito importante. No que diz respeito aos usuários, às receitas, ao nosso Net Promoter
Score®™ e ao crescimento do mercado, se o seu código não funcionar corretamente, ele não vende. É simples assim.
Sabemos por amarga experiência que as falhas lógicas mais simples são muitas vezes as mais fáceis de criar. Os
exemplos com os quais todos podemos nos identificar são aqueles erros únicos, aquele NullPointerException de uma
variável não inicializada e aquela exceção lançada por uma biblioteca que não estava na documentação. Eles são todos
tão simples e pequenos. Parece que seria muito óbvio para nós percebermos que estávamos cometendo esses erros,
mas todos sabemos que eles são muitas vezes os mais difíceis de detectar. Quando nós, humanos, nos concentramos
no panorama geral do nosso código, às vezes esses detalhes críticos simplesmente passam despercebidos.
Sabemos que os testes manuais podem revelar essas falhas lógicas, mas também sabemos por experiência própria que os
planos de testes manuais são frágeis. É possível perder etapas ou apressar-se e perder erros importantes. Poderíamos
simplesmente assumir que algo não precisa de teste nesta versão porque não alteramos essa seção do código. Você adivinhou
– isso nem sempre funciona tão bem para nós. Bugs podem surgir em seções de código que parecem totalmente não
relacionadas ao bug se alguma suposição subjacente tiver mudado.
O teste manual custa dinheiro, que agora não pode ser gasto na adição de novos recursos brilhantes.
Os testes manuais também são responsabilizados por atrasar as datas de envio. Agora, isso é espetacularmente injusto com
nossos colegas de testes manuais. A equipe de desenvolvimento – obviamente escrevendo código sem testes TDD – tropeça
em seus próprios bugs até faltarem apenas alguns dias para o envio. Em seguida, entregamos o código aos testadores, que
precisam executar um enorme documento de teste rapidamente. Às vezes, eles são culpados por atrasar o lançamento, mesmo
que a verdadeira causa tenha sido o desenvolvimento demorando mais do que deveria.
No entanto, nunca tivemos realmente um lançamento. Se definirmos uma versão como incluindo código testado, o que
deveríamos, então fica claro que os testes necessários nunca aconteceram. Você não pode liberar código eticamente quando
nem sabe se ele funciona. Se você fizer isso, seus usuários reclamarão rapidamente.
Não é de admirar que alguns dos meus colegas de teste fiquem tão mal-humorados no final de um sprint.
TDD tem isso totalmente coberto. Esses erros lógicos simplesmente não podem surgir, o que parece fantasia, mas é realmente
verdade.
Antes de digitar qualquer código de produção, você já escreveu um teste com falha. Depois de adicionar seu novo código, você
executa novamente o teste. Se de alguma forma você digitou um erro lógico, o teste ainda falhará e você saberá disso
imediatamente. Essa é a mágica aqui: seu erro acontece, mas é destacado imediatamente. Isso permite que você conserte o
problema quando ainda estiver fresco em sua mente. Isso também significa que você não pode esquecer de consertá-lo mais tarde.
Muitas vezes você pode ir para a linha exata que está errada e fazer a alteração. São 10 segundos de trabalho, não meses de
espera para que um silo de teste comece a trabalhar e preencha um tíquete de bug do JIRA.
Machine Translated by Google
Os tipos de testes unitários de que estamos falando também são rápidos de executar – muito rápidos. Muitos deles são executados
em um milissegundo. Compare isso com o tempo total para escrever um documento de plano de teste, executar o aplicativo inteiro,
configurar os dados armazenados, operar a interface do usuário (UI), registrar a saída e, em seguida, escrever um ticket de bug.
É incomparavelmente melhor, não é?
Você pode ver como esta é uma superpotência para eliminar bugs. Estamos economizando tempo significativamente no ciclo de
teste de código e depuração. Isso reduz os custos de desenvolvimento e aumenta a velocidade de entrega. Estas são grandes
vitórias para nossa equipe e nossos usuários.
Cada vez que você escreve um teste antes do código, você mantém os bugs fora desse código. Você segue a regra mais básica de
não verificar o código com testes que falham. Você os faz passar.
Não deveria ser necessário dizer, mas você também não trapaceia no teste que falhou, excluindo-o, ignorando-o ou fazendo-o
sempre passar usando algum hack técnico. No entanto, estou dizendo tudo isso porque vi exatamente isso sendo feito em código
real.
Vimos como escrever testes primeiro ajuda a evitar a adição de bugs em nosso novo código, mas o TDD é ainda melhor que isso:
ajuda a evitar a adição de bugs no código que adicionaremos no futuro, que abordaremos na próxima seção.
Já vi alguns alunos fazerem isso quando lhes ensinei TDD porque ainda não havia explicado que não deveríamos fazer isso.
Independentemente disso, não excluímos os testes depois que eles são aprovados. Nós mantemos todos eles.
Os testes se transformam em grandes conjuntos de regressão, testando automaticamente todos os recursos do código que construímos.
Ao executar todos os testes com frequência, ganhamos segurança e confiança em toda a base de código.
À medida que os membros da equipe adicionam recursos a essa base de código, manter todos os testes aprovados mostra que
ninguém quebrou algo acidentalmente. É bem possível no software adicionar uma alteração perfeitamente inocente em algum lugar,
apenas para descobrir que alguma coisa aparentemente não relacionada parou de funcionar. Isto será devido à relação entre essas
duas peças que anteriormente não entendíamos.
Os testes agora nos fizeram aprender mais sobre nosso sistema e nossas suposições. Eles impediram que um defeito fosse gravado
na base de código. Ambos são grandes benefícios, mas o panorama geral é que nossa equipe tem confiança para fazer alterações
com segurança e sabe que tem testes cuidando delas automaticamente.
Esta é a verdadeira agilidade, a liberdade de mudar. Agilidade nunca foi uma questão de tickets e sprints do JIRA.
Sempre se tratou da capacidade de avançar rapidamente, com confiança, através de um cenário de requisitos em
constante mudança . Ter dezenas de milhares de testes automatizados de execução rápida é provavelmente a
maior prática facilitadora que temos.
Machine Translated by Google
A capacidade dos testes de dar aos membros da equipe confiança para trabalhar de forma rápida e eficaz é um grande
benefício do TDD. Você deve ter ouvido a frase agir rápido e quebrar coisas, famosa desde os primeiros dias do
Facebook. O TDD nos permite avançar rapidamente e não quebrar as coisas.
Como vimos, os testes são ótimos para fornecer feedback rápido sobre o design e a correção da lógica, bem como fornecer
uma defesa contra erros futuros, mas um enorme benefício extra é que os testes documentam nosso código.
Existe um princípio geral em software de que quanto maior a separação entre duas ideias relacionadas, mais sofrimento elas
trarão. Por exemplo, pense em algum código que lê algum formato de arquivo obscuro do qual ninguém se lembra. Tudo
funciona bem, desde que você esteja lendo arquivos naquele formato antigo. Então você atualiza o aplicativo, aquele formato
de arquivo antigo não é mais suportado e tudo quebra. O código foi separado do conteúdo dos dados nesses arquivos antigos.
Os arquivos não mudaram, mas o código sim. Nem percebemos o que estava acontecendo .
É o mesmo com a documentação. A pior documentação geralmente está contida nas produções mais brilhantes. Esses são
artefatos escritos muito tempo depois de o código ter sido criado por equipes com conjuntos de habilidades separados –
redação, design gráfico e assim por diante. As atualizações da documentação são a primeira coisa a ser descartada de um
lançamento quando o tempo fica apertado.
A solução é aproximar a documentação do código. Faça com que seja produzido por pessoas mais próximas do código e que
conheçam detalhadamente como ele funciona. Faça com que seja lido por pessoas que precisam trabalhar diretamente com esse código.
Tal como acontece com todos os outros aspectos da Extreme Programming (XP), a grande vitória mais óbvia é torná-la tão
próxima do código que é o código. Parte disso envolve usar nossos bons fundamentos de design para escrever código claro
e nosso conjunto de testes também desempenha um papel fundamental.
Nossos testes TDD são códigos, não documentos de teste manuais. Eles geralmente são escritos na mesma linguagem e
repositório da base de código principal. Eles serão escritos pelas mesmas pessoas que estão escrevendo o código de
produção – os desenvolvedores.
Os testes são executáveis. Como forma de documentação, você sabe que algo que pode rodar tem que estar atualizado.
Caso contrário, o compilador irá reclamar e o código não será executado.
Os testes também são o exemplo perfeito de como usar nosso código de produção. Eles definem claramente como deve ser
configurado, quais dependências possui, quais são seus métodos e funções interessantes, quais são os efeitos esperados e
como reportará erros. Tudo o que você gostaria de saber sobre esse código está nos testes.
Machine Translated by Google
Resumo 29
Pode ser surpreendente à primeira vista. Teste e documentação normalmente não são confundidos.
Devido à forma como o TDD funciona, há uma enorme sobreposição entre os dois. Nosso teste é uma descrição detalhada do que
nosso código deve fazer e como podemos fazer isso por nós.
Resumo
Neste capítulo, aprendemos que o TDD nos ajuda a criar bons projetos, escrever a lógica correta, prevenir defeitos futuros e fornecer
documentação executável para nosso código. Entender o que o TDD fará em nossos projetos é importante para utilizá-lo de forma
eficaz e para persuadir nossas equipes a utilizá-lo também. Existem muitas vantagens no TDD, mas ele não é usado com a
frequência que deveria ser em projetos do mundo real.
No próximo capítulo, examinaremos algumas objeções comuns ao TDD, aprenderemos por que elas não são válidas e como
podemos ajudar nossos colegas a superá-las.
Perguntas e respostas
1. Qual é a conexão entre teste e código limpo?
Não existe um código direto, e é por isso que precisamos entender como escrever código limpo. A forma como o TDD
agrega valor é que ele nos força a pensar sobre como nosso código será usado antes de escrevê-lo e quando será mais
fácil limpá-lo. Também nos permite refatorar o nosso código, alterando a sua estrutura sem alterar a sua função, com a
certeza de que não quebramos essa função.
Testes bem escritos substituem parte da documentação, mas não toda. Eles se tornam uma especificação executável
detalhada e atualizada para nosso código. O que eles não podem substituir são documentos como manuais de usuário,
manuais de operações ou especificações contratuais para interfaces de programação de aplicativos (APIs) públicas.
Se escrevermos o código de produção primeiro e depois adicionarmos testes, é mais provável que enfrentemos os seguintes
problemas:
Forçar mais retrabalho quando falhas de projeto são reveladas posteriormente no processo
Machine Translated by Google
Leitura adicional
Uma definição formal de complexidade ciclomática pode ser encontrada no link da WikiPedia. Basicamente,
cada instrução condicional aumenta a complexidade, pois cria um novo caminho de execução possível:
https://en.wikipedia.org/wiki/Cyclomatic_complexity
Machine Translated by Google
3
Dissipando Comum
Mitos sobre TDD
O desenvolvimento orientado a testes (TDD) traz muitos benefícios para os desenvolvedores e para os
negócios. Porém, nem sempre é utilizado em projetos reais. Isso é algo que acho surpreendente. Foi demonstrado
que o TDD melhora a qualidade do código interno e externo em diferentes ambientes industriais. Funciona para
código de front-end e back-end. Funciona em setores verticais. Eu experimentei isso trabalhando em sistemas
embarcados, produtos de webconferência, aplicativos de desktop e frotas de microsserviços.
Para entender melhor como as percepções deram errado, vamos revisar as objeções comuns ao TDD e depois explorar como
podemos superá-las. Ao compreender as dificuldades percebidas, podemos equipar-nos para sermos defensores do TDD e ajudar
os nossos colegas a reformular o seu pensamento. Examinaremos seis mitos populares que cercam o TDD e formaremos respostas
construtivas a eles.
A pesquisa mencionada indica que a recompensa por dedicar mais tempo ao TDD é uma redução no número de defeitos
que entram em operação no software. Com o TDD, esses defeitos são identificados e eliminados muito mais cedo do que
com outras abordagens. Resolvendo problemas antes da garantia de qualidade manual
(QA), implantação e lançamento, e antes de potencialmente enfrentar um relatório de bug de um usuário final, o TDD nos
permite eliminar uma grande parte desse esforço desperdiçado.
Figura 3.1 – Não usar TDD nos deixa mais lentos devido ao retrabalho
A linha superior representa o desenvolvimento de um recurso usando TDD, onde temos testes suficientes para evitar que
quaisquer defeitos entrem em produção. A linha inferior representa o desenvolvimento do mesmo recurso em um estilo
de código e correção, sem TDD, e a descoberta de que um defeito foi ativado na produção. Sem o TDD, descobrimos
falhas muito tarde, irritamos o usuário e pagamos uma grande penalidade de tempo no retrabalho. Observe que a solução
de codificação e correção parece nos levar ao estágio de controle de qualidade mais rapidamente, até considerarmos
todo o retrabalho causado por defeitos não descobertos. O retrabalho é o que não é levado em conta nesse mito.
Usando o TDD, simplesmente tornamos todo o nosso pensamento de design e teste explícito e direto. Nós capturamos e
documentamos usando testes executáveis. Quer escrevamos testes ou não, ainda gastamos o mesmo tempo pensando
em quais são as especificidades que nosso código precisa cobrir. Acontece que a escrita mecânica do código de teste
leva muito pouco tempo. Você mesmo pode medir isso quando escrevermos nosso primeiro teste no Capítulo 5,
Escrevendo nosso primeiro teste. O tempo total gasto escrevendo um trecho de código é o tempo para projetá-lo, mais o
tempo para escrever o código, mais o tempo para testá-lo. Mesmo sem escrever testes automatizados, o tempo de
design e codificação permanecem fatores constantes e dominantes.
A outra área convenientemente ignorada em tudo isso é o tempo necessário para testar manualmente. Sem dúvida,
nosso código será testado. A única questão é quando e por quem. Se escrevermos um teste primeiro, será feito por nós,
os desenvolvedores. Isso acontece antes que qualquer código defeituoso seja verificado em nosso sistema. Se deixarmos
os testes para um colega de teste manual, retardaremos todo o processo de desenvolvimento. Precisamos gastar tempo
ajudando nosso colega a entender quais são os critérios de sucesso do nosso código. Eles devem então elaborar um
plano de teste manual, que muitas vezes deve ser redigido, revisado e aceito na documentação.
Machine Translated by Google
A execução de testes manuais consome muito tempo. Geralmente, todo o sistema deve ser construído e implantado em um
ambiente de teste. Os bancos de dados devem ser configurados manualmente para conter dados conhecidos. A interface do usuário
(IU) deve ser clicado para chegar a uma tela adequada onde nosso novo código pode ser exercido.
A saída deve ser inspecionada manualmente e uma decisão tomada sobre sua correção. Essas etapas devem ser executadas
manualmente sempre que fizermos uma alteração.
Pior ainda, quanto mais tarde deixarmos para testar, maior será a chance de termos construído sobre qualquer código
defeituoso que exista. Não podemos saber se estamos fazendo isso, pois ainda não testamos nosso código. Muitas vezes isso
se torna difícil de desfazer. Em alguns projetos, ficamos tão fora de sintonia com o ramo de código principal que os
desenvolvedores começam a enviar arquivos de patch por e-mail entre si. Isso significa que começamos a construir sobre esse
código defeituoso, tornando-o ainda mais difícil de remover. Estas são práticas ruins, mas ocorrem em projetos reais.
O contraste de escrever primeiro um teste TDD não poderia ser maior. Com o TDD, a configuração é automatizada, as etapas
são capturadas e automatizadas e a verificação dos resultados é automatizada. Estamos falando de reduções na escala de
tempo de minutos para um teste manual até milissegundos usando um teste de unidade TDD. Essa economia de tempo é feita
sempre que precisamos executar esse teste.
Embora os testes manuais não sejam tão eficientes quanto o TDD, ainda existe uma opção muito pior: nenhum teste.
Ter um defeito liberado para produção significa que deixamos que nossos usuários testem o código. Aqui, pode haver
considerações financeiras e o risco de danos à reputação. No mínimo, esta é uma forma muito lenta de descobrir uma falha.
Isolar as linhas de código defeituosas dos logs de produção e dos bancos de dados consome muito tempo. Também geralmente
é frustrante, na minha experiência.
É engraçado como um projeto que nunca encontra tempo para escrever testes unitários sempre encontra tempo para
vasculhar os logs de produção, reverter o código lançado, emitir comunicações de marketing e interromper todos os outros
trabalhos para fazer uma correção de Prioridade 1 (P1) . Às vezes, parece que é mais fácil encontrar dias do que minutos
para algumas abordagens de gerenciamento.
O TDD certamente coloca antecipadamente um custo de tempo na escrita de um teste, mas em troca, ganhamos menos falhas
para corrigir na produção – com uma enorme economia em custo geral, tempo e reputação em comparação com vários ciclos
de retrabalho com defeitos ocorrendo no código ativo.
Sabendo que os testes têm um benefício geral em termos de menos defeitos, vamos examinar outra objeção comum de que
os testes não têm valor, pois não podem prevenir todos os bugs.
Machine Translated by Google
Uma objeção muito antiga a qualquer tipo de teste é esta: você não pode detectar todos os bugs. Embora
isto seja certamente verdade, significa que precisamos de mais e melhores testes, e não menos. Vamos
entender as motivações por trás disso para preparar uma resposta apropriada.
Entendendo por que as pessoas dizem que os testes não conseguem detectar todos os bugs
Imediatamente, podemos concordar com esta afirmação. Os testes não podem detectar todos os bugs. Mais precisamente,
está provado que os testes em sistemas de software só podem revelar a presença de defeitos. Nunca pode provar que
não existem defeitos. Podemos ter muitos testes aprovados e os defeitos ainda podem se esconder nos locais que não
testamos.
Isto parece aplicar-se também a outros campos. Os exames médicos nem sempre revelam problemas muito fracos para
serem notados. Os testes em túnel de vento para aeronaves nem sempre revelam problemas em condições específicas
de voo. A amostragem em lote em uma fábrica de chocolate não detectará todos os doces de qualidade inferior.
Só porque não conseguimos detectar todos os bugs, não significa que isso invalide nossos testes. Cada teste que
escrevemos que detecta um defeito resulta em um defeito a menos em nosso fluxo de trabalho. O TDD nos dá um
processo para nos ajudar a pensar em termos de testes à medida que desenvolvemos, mas ainda existem áreas onde
nossos testes não serão eficazes:
Testes que não escrevemos são um problema real. Mesmo ao escrever testes primeiro no TDD, devemos ser disciplinados
o suficiente para escrever um teste para cada cenário que queremos que funcione. É fácil escrever um teste e depois
escrever o código para fazê-lo passar. A tentação é continuar adicionando código porque estamos em alta. É fácil perder
um caso extremo e não escrever um teste para ele. Se tivermos um teste faltando, abrimos a possibilidade de um defeito
existir e ser encontrado posteriormente.
O problema com as interações no nível do sistema aqui se refere ao comportamento que surge quando você pega
unidades de software testadas e as junta. As interações entre unidades podem às vezes ser mais complexas do que o
previsto. Basicamente, se juntarmos duas coisas bem testadas, a nova combinação em si ainda não foi testada. Algumas
interações apresentam falhas que só aparecem nessas interações, mesmo que as unidades que as compõem tenham
passado em todos os testes.
Esses dois problemas são reais e válidos. O teste nunca cobrirá todas as falhas possíveis, mas isso perde o valor principal
do teste. Cada teste que escrevemos reduzirá um defeito.
Ao não testar nada, nunca detectaremos nada de errado. Não evitaremos quaisquer defeitos. Se testarmos, por menor
que seja, melhoraremos a qualidade do nosso código. Cada defeito que esses testes possam detectar será evitado.
Podemos ver a natureza de espantalho deste argumento: só porque não podemos cobrir todas as eventualidades, isso
não significa que não devamos fazer o que podemos.
Machine Translated by Google
A maneira de reformular isso é termos confiança de que o TDD evita que muitas classes de erros
aconteçam . Nem todos os tipos de erros, certamente, mas um banco de milhares de testes fará
uma melhoria notável na qualidade de nossos aplicativos.
Para explicar isso aos nossos colegas, podemos recorrer a analogias familiares: só porque uma senha forte não pode
impedir todos os hackers, isso não significa que não devemos usar senhas e nos deixar vulneráveis a todo e qualquer
hacker. Manter-se saudável não evitará todos os tipos de problemas médicos, mas evitará muitos tipos de problemas
graves.
Em última análise, esta é uma questão de equilíbrio. O teste zero claramente não é suficiente – todos os defeitos
acabarão sendo ativados neste caso. Sabemos que os testes nunca podem eliminar defeitos. Então, onde devemos
parar? O que constitui suficiente? Podemos argumentar que o TDD nos ajuda a decidir sobre esse equilíbrio no melhor
momento possível: enquanto pensamos em escrever código. Os testes TDD automatizados que criamos nos pouparão
tempo de controle de qualidade manual. É um trabalho manual que não precisa mais ser feito. Essa economia de
tempo e custo aumenta, nos recompensando em cada iteração do código.
Agora que entendemos por que testar o máximo possível sempre é melhor do que não testar, podemos examinar a
próxima objeção comum: como sabemos que os próprios testes foram escritos corretamente?
Uma objeção que você ouvirá é: “Como saberemos se os testes estão corretos se os próprios testes não têm testes?”
Essa objeção foi levantada na primeira vez que apresentei testes unitários a uma equipe. Foi polarizador. Alguns
membros da equipe entenderam o valor imediatamente. Outros eram indiferentes, mas alguns eram ativamente hostis.
Eles viam esta nova prática como uma sugestão de que eram de alguma forma deficientes. Foi percebido como uma ameaça.
Contra esse pano de fundo, um desenvolvedor apontou uma falha na lógica que eu expliquei.
Eu disse à equipe que não podíamos confiar em nossa leitura visual do código de produção. Sim, todos nós temos habilidade
para ler códigos, mas somos humanos, por isso sentimos falta de coisas. Os testes unitários nos ajudariam a evitar perder coisas.
Um desenvolvedor brilhante fez uma ótima pergunta: se a inspeção visual não funciona para código de produção, por
que dizemos que funciona para código de teste? Qual a diferença entre os dois?
A ilustração certa para isso veio depois que precisei testar alguma saída XML (que foi em 2005, eu me
lembro). O código que escrevi para verificar a saída XML era realmente complexo. A crítica estava
correta. Não havia como inspecionar visualmente o código de teste e dizer honestamente que não havia defeitos.
Machine Translated by Google
Então, apliquei TDD ao problema. Usei o TDD para escrever uma classe utilitária que pudesse comparar duas
strings XML e relatar se elas eram iguais ou qual era a primeira diferença. Pode ser configurado para ignorar a
ordem dos elementos XML. Extraí esse código complexo do meu teste original e o substituí por uma chamada
para essa nova classe de utilitário. Eu sabia que a classe utilitária não tinha nenhum defeito, pois ela passou
em todos os testes TDD que escrevi para ela. Houve muitos testes, cobrindo todos os caminhos felizes e todos
os casos extremos que me interessavam. O teste original que havia sido criticado tornou-se agora muito curto e direto.
Pedi ao meu colega que levantou a questão que revisasse o código. Eles concordaram que, nesta forma nova e mais simples,
ficaram felizes em concordar que o teste estava correto, visualmente. Eles acrescentaram a advertência “se a classe de utilidade
funcionar corretamente”. Claro, tínhamos a confiança de que ele passou em todos os testes de TDD contra os quais o havíamos
escrito. Tínhamos certeza de que ele fazia todas as coisas que queríamos especificamente, conforme comprovado pelos testes
para essas coisas.
Na prática, convidamos nossos colegas a apontar onde eles acham que nosso código de teste é complexo demais para ser confiável.
Nós o refatoramos para usar classes utilitárias simples, escritas usando TDD simples. Essa abordagem nos ajuda a construir
confiança, respeita as preocupações válidas de nossos colegas e mostra como podemos encontrar maneiras de reduzir todos os
testes TDD a blocos de código simples e revisáveis.
Agora que já falamos sobre saber se nossos testes estão corretos, outra objeção comum envolve excesso de confiança no TDD:
simplesmente seguir o processo do TDD garantirá um bom código.
Isso pode ser verdade? Vamos examinar os argumentos.
O TDD não tem sugestões sobre a escolha de um nome de variável longo em vez de um nome curto. Não informa
se você deve escolher uma interface ou uma classe abstrata. Você deve optar por dividir um recurso em duas ou
cinco classes? TDD não tem nenhum conselho lá. Você deve eliminar o código duplicado? Inverter uma
dependência? Conectar-se a um banco de dados? Só você pode decidir. TDD não oferece nenhum conselho.
Não é inteligente. Ele não pode substituir você e sua experiência. É um processo simples, que permite validar
suas suposições e ideias.
Na minha opinião, o TDD é extremamente benéfico, mas devemos considerá-lo no contexto. Ele fornece feedback instantâneo sobre nossas
decisões, mas deixa todas as decisões importantes de design de software para nós.
Usando TDD, somos livres para escrever código usando os princípios SOLID (que serão abordados no Capítulo 7,
Driving Design — TDD e SOLID, deste livro) ou podemos usar uma abordagem processual, uma abordagem
orientada a objetos ou uma abordagem funcional. . O TDD nos permite escolher nosso algoritmo como acharmos adequado.
Isso nos permite mudar de ideia sobre como algo deve ser implementado. TDD funciona em todas as linguagens de programação. Funciona em
todas as verticais.
Ajudar nossos colegas a enxergar além dessa objeção os ajuda a perceber que o TDD não é um sistema mágico que substitui a inteligência e a
habilidade do programador. Ele aproveita essa habilidade, fornecendo feedback instantâneo sobre nossas decisões. Embora isso possa
decepcionar colegas que esperavam que isso permitiria que o código perfeito surgisse de um pensamento imperfeito, podemos salientar que o
TDD nos dá tempo para pensar. A vantagem é que coloca o pensamento e o design na frente e no centro. Ao escrever um teste com falha antes
de escrever o código de produção que faz o teste passar, garantimos que pensamos sobre o que esse código deveria fazer e como deveria ser
Dado que entendemos que o TDD não projeta nosso código para nós, mas ainda é amigo do desenvolvedor, como podemos abordar o teste de
código complexo?
Os desenvolvedores profissionais lidam rotineiramente com códigos altamente complexos. Isso é apenas um fato da vida. Isso leva a uma objeção
válida: nosso código é muito difícil de escrever testes unitários. O código em que trabalhamos pode ser um código legado altamente valioso e
confiável, que gera receitas significativas. Este código pode ser complexo.
Mas é muito complexo para testar? É verdade que cada pedaço de código complexo simplesmente não pode ser testado?
A resposta está nas três maneiras pelas quais o código se torna complexo e difícil de testar:
• Complexidade acidental: escolhemos um caminho difícil em vez de um caminho mais simples por acidente
• Sistemas externos não podem ser controlados para serem configurados para nossos testes
A complexidade acidental torna o código difícil de ler e difícil de testar. A melhor maneira de pensar sobre isso é saber
que qualquer problema tem muitas soluções válidas. Digamos que queremos adicionar um total de cinco números.
Poderíamos escrever um loop. Poderíamos criar cinco tarefas simultâneas que recebam cada número e, em seguida,
reportar esse número para outra tarefa simultânea que calcule o total (tenha paciência, por favor... já vi isso acontecer).
Poderíamos ter um sistema complexo baseado em padrões de design em que cada número aciona um observador, que
coloca cada um em uma coleção, que aciona um observador para adicionar ao total, que aciona um observador a cada
10 segundos após a última entrada.
Sim, eu sei que alguns deles são bobos. Eu acabei de inventá-los. Mas sejamos honestos – em que tipos
de designs bobos você já trabalhou antes? Eu sei que escrevi um código mais complexo do que precisava ser.
O ponto principal do exemplo da adição de cinco números é que ele realmente deve usar um loop simples.
Qualquer outra coisa é complexidade acidental, nem necessária nem intencional. Por que faríamos isso?
Existem muitas razões. Pode haver algumas restrições do projeto, uma diretriz de gestão ou simplesmente uma
preferência pessoal que orienta a nossa decisão. Seja como for, uma solução mais simples era possível, mas não a
aproveitámos.
Testar soluções mais complexas geralmente requer testes mais complexos. Às vezes, nossa equipe acha que não vale
a pena perder tempo com isso. O código é complexo, será difícil escrever testes e achamos que já funciona. Achamos
que é melhor não tocá-lo.
Sistemas externos causam problemas nos testes. Suponha que nosso código se comunique com um serviço web de
terceiros. É difícil escrever um teste repetível para isso. Nosso código consome o serviço externo e os dados que ele
nos envia são diferentes a cada vez. Não podemos escrever um teste e verificar o que o serviço nos enviou, pois não
sabemos o que o serviço deveria nos enviar. Se pudéssemos substituir esse serviço externo por algum serviço fictício
que pudéssemos controlar, poderíamos resolver esse problema facilmente. Mas se nosso código não permitir isso,
ficaremos presos.
O código emaranhado é um desenvolvimento adicional disso. Para escrever um teste, precisamos entender o que esse
código faz com uma condição de entrada: o que esperamos que sejam as saídas? Se tivermos um corpo de código que
simplesmente não entendemos, não poderemos escrever um teste para ele.
Embora esses três problemas sejam reais, há uma causa subjacente a todos eles: permitimos que nosso software
chegasse a esse estado. Poderíamos ter organizado isso para usar apenas algoritmos e estruturas de dados simples.
Poderíamos ter isolado sistemas externos para poder testar o resto do código sem eles. Poderíamos ter modularizado
nosso código para que não ficasse excessivamente emaranhado.
Todos os problemas anteriores estão relacionados à criação de software que funciona, mas que não segue boas
práticas de design. A maneira mais eficaz de mudar isso, na minha experiência, é a programação em pares – trabalhando
juntos no mesmo trecho de código e ajudando uns aos outros a encontrar essas melhores ideias de design. Se par
Machine Translated by Google
a programação não é uma opção, então as revisões de código também fornecem um ponto de verificação para introduzir designs melhores.
O emparelhamento é melhor porque, no momento da revisão do código, pode ser tarde demais para fazer alterações
importantes. É mais barato, melhor e mais rápido prevenir um design deficiente do que corrigi-lo.
O melhor conselho aqui é simplesmente deixar esse código de lado, se possível. Às vezes, porém, precisamos adicionar
recursos que exigem que o código seja alterado. Dado que não temos testes existentes, é bastante provável que
descobriremos que adicionar um novo teste é praticamente impossível. O código simplesmente não está dividido de uma
forma que nos dê pontos de acesso para realizar um teste.
Neste caso, podemos utilizar a técnica do Teste de Caracterização . Podemos descrever isso em três etapas:
2. Registre todas as saídas resultantes de cada uma dessas execuções de entrada. Esta saída é tradicionalmente
chamada de Golden Master.
3. Escreva um teste de caracterização que execute o código com todas as entradas novamente. Compare cada
resultado com o Golden Master capturado. O teste falhará se algum for diferente.
Este teste automatizado compara quaisquer alterações feitas no código com o que o código original fez.
Isso nos guiará enquanto refatoramos o código legado. Podemos usar técnicas de refatoração padrão
combinadas com TDD. Ao preservar as saídas defeituosas no Golden Master, garantimos que estamos
refatorando puramente nesta etapa. Evitamos a armadilha de reestruturar o código ao mesmo tempo que
corrigimos os bugs. Quando bugs estão presentes no código original, trabalhamos em duas fases distintas:
primeiro, refatorar o código sem alterar o comportamento observável. Depois, corrija os defeitos como uma tarefa separada.
Nunca corrigimos bugs e refatoramos juntos. O Teste de Caracterização garante que não confundamos acidentalmente as
duas tarefas.
Vimos como o TDD ajuda a lidar com a complexidade acidental e a dificuldade de alterar código legado.
Certamente escrever um teste antes do código de produção significa que precisamos saber como é o código antes de
testá-lo. Vamos revisar essa objeção comum a seguir.
Uma grande frustração para os alunos de TDD é saber o que testar sem ter escrito o código de produção previamente.
Esta é outra crítica que tem mérito. Nesse caso, uma vez que entendemos o problema que os desenvolvedores enfrentam,
podemos ver que a solução é uma técnica que podemos aplicar ao nosso fluxo de trabalho, e não uma reformulação do
pensamento.
Machine Translated by Google
Até certo ponto, é natural pensar em como implementamos o código. Afinal, é assim que aprendemos.
Escrevemos System.out.println("Olá, Mundo!"); em vez de pensar em alguma estrutura para colocar em
torno da famosa linha. Pequenos programas e utilitários funcionam bem quando os escrevemos como
código linear, semelhante a uma lista de compras de instruções.
Começamos a enfrentar dificuldades à medida que os programas ficam maiores. Precisamos de ajuda para
organizar o código em partes compreensíveis. Esses pedaços precisam ser fáceis de entender. Queremos que
eles sejam autodocumentados e que seja fácil sabermos como chamá-los. Quanto maior o código fica, menos
interessante é o interior desses pedaços e mais importante se torna a estrutura externa desses pedaços – o lado de fora.
Por exemplo, digamos que estamos escrevendo uma classe TextEditorWidget e queremos verificar a ortografia
rapidamente. Encontramos uma biblioteca com uma classe SpellCheck . Não nos importamos muito com o funcionamento
da classe SpellCheck . Nós apenas nos preocupamos em como podemos usar esta classe para verificar a ortografia.
Queremos saber como criar um objeto dessa classe, quais métodos precisamos chamar para que ele faça seu trabalho
de verificação ortográfica e como podemos acessar a saída.
Esse tipo de pensamento é a definição de design de software – como os componentes se encaixam. É fundamental
enfatizarmos o design à medida que as bases de código crescem, se quisermos mantê-las. Usamos encapsulamento
para ocultar os detalhes das estruturas de dados e algoritmos dentro de nossas funções e classes. Fornecemos uma
interface de programação simples de usar.
Decisões de projeto de andaimes TDD. Ao escrever o teste antes do código de produção, definimos como queremos que
o código em teste seja criado, chamado e usado. Isso nos ajuda a ver rapidamente o quão bem nossas decisões estão
funcionando. Se o teste mostrar que criar nosso objeto é difícil, isso nos mostra que nosso design deve simplificar a
etapa de criação. O mesmo se aplica se o objeto for difícil de usar; devemos simplificar nossa interface de programação
como resultado.
No entanto, como podemos lidar com os tempos em que simplesmente ainda não sabemos o que deveria ser um design
razoável? Essa situação é comum quando usamos uma nova biblioteca, integramos com algum código novo do restante
de nossa equipe ou abordamos uma grande história de usuário.
Para resolver isso, usamos um pico, uma pequena seção de código que é suficiente para provar a forma de um design.
Não pretendemos o código mais limpo neste estágio. Não cobrimos muitos casos extremos ou condições de erro.
Temos o objectivo específico e limitado de explorar uma possível disposição de objectos e funções para fazer um design
credível. Assim que tivermos isso, esboçamos algumas notas sobre o design e depois o excluímos. Agora que sabemos
como é um design razoável, estamos em melhor posição para saber quais testes escrever. Agora podemos usar TDD
normal para conduzir nosso design.
Curiosamente, quando recomeçamos desta forma, muitas vezes acabamos por obter um design melhor do que o nosso
pico. O ciclo de feedback do TDD nos ajuda a identificar novas abordagens e melhorias.
Machine Translated by Google
Resumo 41
Vimos como é natural querer começar a implementar o código antes dos testes e como podemos usar TDD e picos para
criar um processo melhor. Tomamos decisões no último momento responsável – o mais tardar possível para decidir antes
de tomarmos conscientemente uma decisão irreversível e inferior. Em caso de dúvida, podemos aprender mais sobre o
espaço de soluções usando um pico – um pequeno pedaço de código experimental projetado para aprender e depois
descartar.
Resumo
Neste capítulo, aprendemos seis mitos comuns que impedem as equipes de usar TDD e discutimos a abordagem correta
para reformular essas conversas. O TDD realmente merece uma aplicação muito mais ampla no desenvolvimento de
software moderno do que tem agora. Não é que as técnicas não funcionem. O TDD simplesmente tem um problema de
imagem, muitas vezes entre pessoas que ainda não experimentaram seu verdadeiro poder.
Na segunda parte deste livro, começaremos a colocar em prática os vários ritmos e técnicas do TDD e a construir uma
pequena aplicação web. No próximo capítulo, iniciaremos nossa jornada TDD com o básico para escrever um teste unitário
com o padrão Arrange-Act-Assert (AAA) .
Perguntas e respostas
1. Por que se acredita que o TDD retarda os desenvolvedores?
Quando não escrevemos um teste, economizamos o tempo gasto escrevendo o teste. O que isso não leva em
consideração são os custos de tempo extra para encontrar, reproduzir e corrigir um defeito na produção.
Não. Muito pelo contrário. Ainda projetamos nosso código usando todas as técnicas de design à nossa disposição.
O que o TDD nos dá é um rápido ciclo de feedback sobre se nossas escolhas de design resultaram em um código
correto e fácil de usar.
Que pergunta fantástica para fazer a eles! Seriamente. Veja se alguma de suas objeções foi abordada neste
capítulo. Nesse caso, você pode conduzir a conversa com delicadeza usando as ideias apresentadas.
Leitura adicional
• https://en.wikipedia.org/wiki/Characterization_test
Mais detalhes sobre a técnica de Teste de Caracterização, onde capturamos a saída de um módulo de software
existente exatamente como está, com o objetivo de reestruturar o código sem alterar seu comportamento. Isto é
especialmente valioso em códigos mais antigos, onde os requisitos originais se tornaram pouco claros ou que
evoluíram ao longo dos anos para conter defeitos dos quais outros sistemas agora dependem.
• https://efficientsoftwaredesign.com/2014/03/27/lean-software development-before-and-
after-the-last-responsible-moment/
Uma análise aprofundada do que significa decidir no último momento responsável para o design de software.
Machine Translated by Google
Machine Translated by Google
Parte 2:
Técnicas TDD
A Parte 2 apresenta as técnicas necessárias para um TDD eficaz. Ao longo do caminho, construiremos gradativamente a lógica
central de um jogo de adivinhação de palavras, Wordz – escrevendo todos os nossos testes primeiro.
Ao final desta parte, teremos produzido código de alta qualidade escrevendo primeiro os testes. Os princípios SOLID e a arquitetura
hexagonal nos ajudarão a organizar o código em blocos de construção bem projetados e fáceis de testar. Os testes duplos trarão
dependências externas sob nosso controle. Veremos o panorama geral da automação de testes e como a pirâmide de testes, os
engenheiros de controle de qualidade e o fluxo de trabalho melhoram nosso trabalho.
4
Construindo um aplicativo
Usando TDD
Aprenderemos o lado prático do TDD construindo primeiro o teste da aplicação. Também usaremos uma abordagem
conhecida como desenvolvimento ágil de software à medida que construímos. Ser ágil significa construir nosso
software em iterações pequenas e independentes, em vez de construí-lo tudo de uma vez. Essas pequenas etapas nos
permitem aprender mais sobre o design do software à medida que avançamos. Adaptamos e refinamos o design ao
longo do tempo, à medida que temos mais certeza de como um bom design pode parecer. Podemos oferecer
funcionalidades funcionais para usuários de teste iniciais e receber seus comentários muito antes de o aplicativo ser concluído. Isto é valioso.
Como vimos nos capítulos anteriores, o TDD é uma abordagem excelente para fornecer feedback rápido sobre peças
de software independentes. É o complemento perfeito para o desenvolvimento ágil.
Para nos ajudar a construir dessa forma, este capítulo apresentará a técnica de histórias de usuários, que é uma
forma de capturar requisitos que se adapta bem a uma abordagem ágil. Prepararemos nosso ambiente de
desenvolvimento Java para o desenvolvimento de teste antes de descrever o que nosso aplicativo fará.
Requerimentos técnicos
O código final deste capítulo pode ser encontrado em https://github.com/PacktPublishing/
Desenvolvimento orientado a testes com Java/tree/main/chapter04.
Para codificar – o que eu recomendo fortemente – precisamos primeiro configurar nosso ambiente de desenvolvimento.
Isso usará o excelente ambiente de desenvolvimento integrado (IDE) JetBrains IntelliJ Java, um Java
SDK gratuito da Amazon e algumas bibliotecas para nos ajudar a escrever nossos testes e incluir as
bibliotecas em nosso projeto Java. Reuniremos todas as nossas ferramentas de desenvolvimento na próxima seção.
Machine Translated by Google
Começaremos instalando nosso IDE Java, o JetBrains IntelliJ IDE Community Edition, antes de adicionar o restante
das ferramentas.
Para nos ajudar a trabalhar com o código-fonte Java, usaremos o JetBrains IntelliJ Java IDE, usando sua Community
Edition gratuita. Este é um IDE popular usado na indústria de software – e por um bom motivo. Ele combina um
excelente editor Java com preenchimento automático e sugestões de código, juntamente com um depurador, suporte
para refatoração automatizada, ferramentas de controle de origem Git e excelente integração para execução de testes.
1. Acesse https://www.jetbrains.com/idea/download/.
Depois de concluído, o IDE IntelliJ deverá ser instalado em seu computador. A próxima etapa é criar um projeto Java
vazio, usando o sistema de gerenciamento de pacotes Gradle, e então configurar a versão do Java que desejamos
usar. As instalações para Mac, Windows e Linux geralmente são simples.
Depois que o IntelliJ estiver instalado, podemos importar o projeto inicial fornecido no repositório GitHub que o
acompanha. Isso configurará um projeto Java que usa o Amazon Corretto 17 Java Development Kit
(JDK), o executor de testes de unidade JUnit 5, o sistema de gerenciamento de compilação Gradle e a biblioteca de
asserções fluentes AssertJ.
Machine Translated by Google
Requerimentos técnicos 47
2. Use sua ferramenta git preferida para clonar todo o repositório em seu computador. Se você usar o
ferramenta de linha de comando git , será o seguinte:
4. Clique em Abrir e navegue até a pasta Chapter04 do repositório que acabamos de clonar.
Clique para destacá-lo:
6. Aguarde o IntelliJ importar os arquivos. Você deverá ver este espaço de trabalho aberto:
Agora temos o IDE configurado com um projeto esqueleto contendo tudo o que precisamos para começar.
Na próxima seção descreveremos os principais recursos da aplicação que iremos construir, o que começaremos a
fazer no próximo capítulo.
O jogador pode usar esse feedback para fazer uma próxima estimativa melhor. Depois que um jogador adivinha a
palavra corretamente, ele ganha alguns pontos. Eles ganham seis pontos por um palpite correto na primeira tentativa,
cinco pontos por um palpite correto na segunda tentativa e um ponto por um palpite correto na sexta e última tentativa.
Os jogadores competem entre si em várias rodadas para obter a pontuação mais alta. Wordz é um jogo divertido e também
um exercício cerebral suave.
Embora construir uma interface de usuário esteja fora do escopo deste livro, é muito útil ver um exemplo possível:
Tecnicamente, vamos criar o componente de serviço web backend para este jogo. Ele exporá uma
Interface de Programação de Aplicativo (API) para que uma interface de usuário possa usar o serviço
e acompanhar o estado do jogo em um banco de dados.
Para focar nas técnicas de TDD, deixaremos algumas coisas fora do nosso escopo, como autenticação do usuário e
interface do usuário. Uma versão de produção incluiria, é claro, esses aspectos.
Mas para implementar esses recursos, não precisamos de nenhuma nova técnica de TDD.
Este design simples nos permitirá explorar completamente o TDD através de todas as camadas de uma aplicação web típica.
Agora que definimos o que iremos construir, a próxima seção apresentará a abordagem de desenvolvimento que usaremos
para construí-lo.
Machine Translated by Google
O antecessor do ágil é chamado de desenvolvimento em cascata. É chamado assim porque as etapas do projeto
fluem como uma cascata, cada uma delas é totalmente concluída antes que a próxima seja iniciada.
1. Coleta de requisitos
5. Testando o código
Em teoria, todas as etapas são executadas perfeitamente, tudo funciona e não há problemas. Na realidade, sempre há
problemas.
Descobrimos certos requisitos que havíamos perdido. Descobrimos que os documentos de projeto não podem ser codificados
exatamente como foram escritos. Encontramos partes faltantes do design. A codificação em si pode apresentar dificuldades. A
pior parte é que o usuário final nunca vê nenhum software funcionando até o final. Se o que eles veem não é o que tinham em
mente, temos um conjunto muito caro de mudanças e retrabalhos a fazer.
A razão para isso é que os humanos têm uma visão limitada. Por mais que tentemos, não podemos prever o futuro com
precisão. Posso sentar aqui com uma xícara de café quente e saber com certeza que esfriará em vinte minutos. Mas não posso
dizer como estará o tempo daqui a três meses. A nossa capacidade de prever o futuro está limitada a prazos curtos, para
processos com causas e efeitos bem definidos.
O desenvolvimento em cascata tem um desempenho muito fraco face à incerteza e à mudança. Ele foi projetado em torno da
noção de que todas as coisas podem ser conhecidas e planejadas com antecedência. Uma abordagem melhor é abraçar a
mudança e a incerteza, tornando-as uma parte ativa do processo de desenvolvimento. Esta é a base do desenvolvimento ágil.
Em sua essência está uma abordagem iterativa, onde pegamos um pequeno recurso que interessa aos nossos usuários e, em
seguida, construímos esse recurso completamente, permitindo que nossos usuários o experimentem. Se forem necessárias
alterações , fazemos outra iteração de desenvolvimento. Os custos da mudança são muito mais baixos quando o nosso
processo de desenvolvimento apoia ativamente a mudança.
Os processos profissionais de desenvolvimento ágil dependem da manutenção de uma única base de código que é sempre testada e
representa a melhor versão até o momento do nosso software. Este código está sempre pronto para ser implantado nos usuários.
Aumentamos essa base de código, um recurso de cada vez, melhorando continuamente seu design à medida que avançamos.
Machine Translated by Google
Técnicas como TDD desempenham um papel importante nisso, garantindo que nosso código seja bem projetado e
exaustivamente testado. Cada vez que enviamos código para o tronco principal, já sabemos que ele passou em muitos testes de TDD.
Sabemos que estamos felizes com seu design.
Para melhor apoiar o desenvolvimento iterativo, escolhemos uma técnica iterativa para capturar requisitos.
Essa técnica é chamada de histórias de usuários, que descreveremos na próxima seção.
Através de técnicas ágeis, não precisamos conhecer o futuro antecipadamente; podemos descobri-lo ao lado
nossos usuários.
Apoiar esta mudança é uma nova forma de expressar requisitos. Os projetos em cascata começam com um documento
de requisitos completo, detalhando formalmente cada recurso. O conjunto completo de requisitos – muitas vezes milhares
deles – foi expresso em linguagem formal como “O sistema deve…” e depois os detalhes foram explicados em termos
de alterações no sistema de software. Com o desenvolvimento ágil, não queremos capturar requisitos dessa forma.
Queremos capturá-los seguindo dois princípios fundamentais:
A técnica para fazer isso é chamada de história do usuário. A primeira história de usuário a ser abordada para Wordz é
a seguinte:
As três seções foram escritas desta forma para enfatizar que o desenvolvimento ágil gira em torno do valor entregue aos
usuários do sistema. Estes não são requisitos técnicos. Eles não especificam (na verdade, não devem) especificar uma
solução. Eles simplesmente declaram qual usuário do sistema deve obter qual resultado valioso dele.
A primeira parte sempre começa com “Como….” Em seguida, nomeia a função do usuário que esta história irá melhorar.
Pode ser qualquer usuário – seja humano ou máquina – do sistema. A única coisa que nunca deve ser é o próprio sistema,
como em “Como um sistema”. Isso é para impor um pensamento claro em nossas histórias de usuários; eles devem sempre
entregar algum benefício a algum usuário do sistema. Eles nunca são um fim em si mesmos.
Para dar um exemplo de um aplicativo para tirar fotos, como desenvolvedores, podemos querer uma atividade técnica para
otimizar o armazenamento de fotos. Poderíamos escrever uma história como: “Como sistema, quero compactar meus dados
de imagem para otimizar o armazenamento”. Em vez de escrever de um ponto de vista técnico, podemos reformular isto
para destacar o benefício para o utilizador: “Como fotógrafo, quero acesso rápido às minhas fotografias armazenadas e
maximizar o espaço para novas”.
A seção “Eu quero…” descreve o resultado desejado que o usuário deseja. É sempre descrito na terminologia do usuário,
não na terminologia técnica. Novamente, isso nos ajuda a focar no que nossos usuários desejam que nosso software alcance
para eles. É a forma mais pura de capturar os requisitos. Não há nenhuma tentativa feita nesta fase de sugerir como algo
será implementado. Simplesmente capturamos o que o usuário pretende fazer.
A parte final, “…para que…”, fornece contexto. A seção “Como…” descreve quem se beneficia, a seção “Eu quero…”
descreve como eles se beneficiam e a seção “…para que…” descreve por que eles precisam desse recurso. Isso justifica o
tempo e os custos necessários para desenvolver esse recurso. Ele pode ser usado para priorizar quais recursos serão
desenvolvidos a seguir.
Esta história de usuário é onde iniciamos o desenvolvimento. O coração do aplicativo Wordz é sua capacidade de avaliar e
pontuar a suposição atual do jogador sobre uma palavra. Vale a pena ver como esse trabalho irá prosseguir.
TDD é um complemento perfeito para o desenvolvimento ágil. Como aprendemos nos capítulos anteriores, o TDD nos ajuda
a melhorar nosso projeto e a provar que nossa lógica está correta. Tudo o que fazemos tem como objetivo entregar software
funcional aos nossos usuários, sem defeitos, o mais rápido possível. TDD é uma ótima maneira de conseguir isso.
Machine Translated by Google
Resumo 53
4. Use TDD para escrever código para conectar o núcleo a um banco de dados.
Este processo se repete. Ele forma o ritmo de escrever a lógica principal do aplicativo em um teste de unidade e, em seguida,
expandir o aplicativo, conectando-o a endpoints de API, interfaces de usuário, bancos de dados e serviços da Web externos.
Trabalhando dessa forma, mantemos muita flexibilidade em nosso código. Também podemos trabalhar rapidamente,
concentrando-nos antecipadamente nas partes mais importantes do código da nossa aplicação.
Resumo
Aprendemos as principais ideias que nos permitem construir um aplicativo de forma iterativa, obtendo valor em cada etapa e
evitando uma grande abordagem inicial de design que muitas vezes decepciona. Podemos ler histórias de usuários, o que
impulsionará a construção de nosso aplicativo TDD em etapas pequenas e bem definidas. Agora também conhecemos o
processo que usaremos para construir nosso aplicativo – usando TDD para obter um núcleo central de código limpo
exaustivamente testado e, em seguida, eliminar conexões com o mundo real.
No próximo capítulo, daremos início à nossa aplicação. Aprenderemos os três componentes principais de cada teste TDD
escrevendo nosso primeiro teste e garantindo que ele seja aprovado.
Perguntas e respostas
1. O desenvolvimento em cascata parece que deveria funcionar bem – por que não funciona?
O desenvolvimento em cascata funcionaria bem se soubéssemos de cada requisito ausente, de cada solicitação
de mudança dos usuários, de cada má decisão de design e de cada erro de codificação no início do projeto. Mas os
humanos têm uma visão limitada e é impossível saber estas coisas com antecedência. Portanto, projetos em
cascata nunca funcionam bem. Mudanças caras surgem em um estágio posterior do projeto – justamente quando
você não tem tempo para resolvê-las.
Sim, embora assim percamos as vantagens do TDD que abordamos nos capítulos anteriores. Também tornamos
nosso trabalho mais difícil. Uma parte importante do desenvolvimento Agile é sempre demonstrar o código
funcional mais recente. Sem o TDD, precisamos adicionar um grande ciclo de testes manuais ao nosso processo.
Isso nos retarda significativamente.
Machine Translated by Google
Leitura adicional
• Dominando o desenvolvimento orientado a testes do React, ISBN 9781789133417
Se você deseja construir uma interface de usuário para o aplicativo Wordz, usar a popular estrutura React web
UI é uma excelente maneira de fazer isso. Este livro Packt é um dos meus favoritos. Ele mostra como aplicar o mesmo
tipo de técnicas de TDD que usamos no lado do servidor no trabalho de front-end. Ele também explica o
desenvolvimento do React desde o início de uma forma altamente legível.
Este livro fornece mais detalhes sobre como criar histórias de usuários eficazes e outras técnicas úteis para
capturar requisitos, modelagem e análise ágeis.
Machine Translated by Google
5
Escrevendo nosso primeiro teste
É hora de mergulharmos e escrevermos nosso primeiro teste de unidade TDD neste capítulo. Para nos ajudar a fazer
isso, aprenderemos sobre um modelo simples que nos ajuda a organizar cada teste em um código lógico e legível.
Ao longo do caminho, aprenderemos alguns princípios-chave que podemos usar para tornar nossos testes eficazes.
Veremos como escrever o teste primeiro nos força a tomar decisões sobre o design do nosso código e sua facilidade de
uso, antes de precisarmos pensar nos detalhes de implementação.
Depois de alguns exemplos cobrindo essas técnicas, começaremos nosso aplicativo Wordz, escrevendo um teste primeiro
antes de adicionar o código de produção para fazer esse teste passar. Usaremos as populares bibliotecas de teste de
unidade Java JUnit5 e AssertJ para nos ajudar a escrever testes fáceis de ler.
Neste capítulo, cobriremos os seguintes princípios principais por trás da escrita de testes unitários eficazes:
• Afirmando exceções
Requerimentos técnicos
O código final deste capítulo pode ser encontrado em https://github.com/PacktPublishing/
Desenvolvimento orientado a testes com Java/tree/main/chapter05.
Machine Translated by Google
Para explicar o que cada seção faz, vamos percorrer um teste de unidade completo para um trecho de código onde
queremos garantir que um nome de usuário seja exibido em letras minúsculas:
importar org.junit.jupiter.api.Test;
importar static org.assertj.core.api.Assertions.*;
@Teste
assertThat(real).isEqualTo("sirjakington35179");
}
}
Machine Translated by Google
A primeira coisa a notar é o nome da classe do nosso teste: UsernameTest. Esta é a primeira narrativa para
leitores de nosso código. Estamos descrevendo a área comportamental que estamos testando, neste caso,
nomes de usuários. Todos os nossos testes, e na verdade todo o nosso código, devem seguir esta abordagem
narrativa: o que queremos que os leitores do nosso código entendam? Queremos que eles vejam claramente
qual é o problema que estamos resolvendo e como deve ser usado o código que o resolve. Queremos demonstrar
a eles que o código funciona corretamente.
Dentro do método @Test , podemos ver nossa estrutura Arrange-Act-Assert. Primeiro organizamos para que
nosso código possa ser executado. Isso envolve a criação de quaisquer objetos necessários, o fornecimento
de qualquer configuração necessária e a conexão de quaisquer objetos e funções dependentes. Às vezes, não
precisamos desta etapa, por exemplo, se estivermos testando uma função independente simples. Em nosso
código de exemplo, a etapa Arrange é a linha que cria o objeto username e fornece um nome ao construtor.
Em seguida, ele armazena esse objeto pronto para uso na variável local nome de usuário . É a primeira linha
do var username = new Username("SirJakington35179"); corpo do método de teste.
A etapa Agir segue. Esta é a parte em que fazemos com que nosso código em teste atue – nós executamos esse código. Esta é sempre uma
chamada para o código em teste, fornecendo quaisquer parâmetros necessários e organizando a captura dos resultados. No exemplo, a String
actual = username.asLowerCase(); linha é a etapa Act. Chamamos o método asLowerCase() em nosso objeto nome de usuário . Não aceita
parâmetros e retorna um objeto String simples contendo o texto em letras minúsculas sirjakington35179 como resultado.
Testes de unidade como esse são fáceis de escrever, fáceis de ler e executados muito rapidamente. Muitos desses testes podem ser executados
em menos de 1 segundo.
A biblioteca JUnit é a estrutura de teste de unidade padrão do setor para Java. Ele nos fornece um meio de anotar
métodos Java como testes de unidade, nos permite executar todos os nossos testes e exibir visualmente os resultados,
conforme mostrado aqui na janela do IntelliJ IDE:
Machine Translated by Google
Vemos aqui que o teste de unidade falhou. O teste esperava que o resultado fosse a string de texto
sirjakington35179 , mas em vez disso, recebemos nulo. Usando TDD, completaríamos apenas o código suficiente
para fazer o teste passar:
Podemos ver que nossa mudança no código de produção fez esse teste passar. Tornou-se verde, para usar o termo
popular. Os testes que falham são descritos como testes vermelhos e os que passam são verdes. Isso se baseia nas cores
mostradas em IDEs populares, que, por sua vez, são baseados em sinais de trânsito. Ver todas essas iterações curtas de
testes vermelhos se transformando em verdes é surpreendentemente satisfatório, além de aumentar a confiança em nosso
trabalho. Os testes nos ajudam a focar no design do nosso código, forçando-nos a trabalhar retroativamente a partir dos resultados.
Vejamos o que isso significa.
Uma coisa que notamos imediatamente é o quão sem importância é o código real que faz esse teste passar.
Tudo neste teste trata de definir as expectativas desse código. Estamos estabelecendo limites sobre por que
nosso código é útil e o que esperamos que ele faça. Não estamos restringindo de forma alguma como isso acontece.
Estamos adotando uma visão de fora para dentro do código. Qualquer implementação que faça nosso teste passar é aceitável.
Machine Translated by Google
Este parece ser um ponto de transição no aprendizado do uso do TDD. Muitos de nós aprendemos a programar escrevendo
primeiro as implementações. Pensamos em como o código funcionaria. Aprofundamos os algoritmos e estruturas de dados
por trás de uma implementação específica. Então, como último pensamento, envolvemos tudo em algum tipo de interface
que pode ser chamada.
TDD vira isso de cabeça para baixo. Projetamos intencionalmente nossa interface chamável primeiro, pois é isso que os
usuários desse código verão. Usamos o teste para descrever com precisão como o código será configurado, como será
chamado e o que podemos esperar que ele faça por nós. Depois que nos acostumamos a fazer esse design de fora para
dentro primeiro, o TDD segue muito naturalmente e melhora a eficiência do nosso fluxo de trabalho de várias maneiras
importantes. Vamos revisar quais são essas melhorias.
Testes unitários como esses aumentam nossa eficiência como desenvolvedores de diversas maneiras. O mais óbvio é que
o código que escrevemos passou num teste: sabemos que funciona. Não estamos esperando por um processo manual de
controle de qualidade para encontrar um defeito e, em seguida, gerar um relatório de bug para retrabalho no futuro. Nós
encontramos e corrigimos bugs agora, antes mesmo de liberá-los no tronco de origem principal, muito menos para os
usuários. Documentamos nossas intenções para nossos colegas. Se alguém quiser saber como nossa classe Username
funciona, está ali mesmo no teste – como você cria o objeto, quais métodos você pode chamar e quais serão os resultados esperados.
Os testes de unidade nos fornecem uma maneira de executar o código isoladamente. Não somos mais forçados a
reconstruir um aplicativo inteiro, executá-lo, configurar entradas de dados de teste em nosso banco de dados, fazer login
na interface do usuário, navegar até a tela correta e inspecionar visualmente a saída do nosso código. Executamos o teste.
É isso. Isso nos permite executar código que ainda não está totalmente integrado ao tronco principal da nossa aplicação.
Isso agiliza nosso trabalho. Podemos começar mais rapidamente, gastar mais tempo desenvolvendo o código disponível e
gastar menos tempo em testes manuais e processos de implantação complicados.
Um benefício adicional é que este ato de design melhora a modularidade do nosso código. Ao projetar código que pode ser
testado em pequenos pedaços, nos lembramos de escrever código que possa ser executado em pequenos pedaços. Essa
tem sido a abordagem básica do design desde a década de 1960 e continua tão eficaz hoje como sempre foi.
Esta seção cobriu a estrutura padrão que usamos para organizar cada teste unitário, mas não garante que escreveremos
um bom teste. Para conseguir isso, cada teste precisa ter propriedades específicas.
Os PRIMEIROS princípios descrevem as propriedades de um bom teste. Vamos aprender como aplicá-los a seguir.
Como todo código, o código de teste unitário pode ser escrito de maneiras melhores ou piores. Vimos como o AAA nos
ajuda a estruturar um teste corretamente e como nomes descritivos e precisos contam a história do que pretendemos que
nosso código faça. Os testes mais úteis também seguem os princípios FIRST e usam uma afirmação por teste.
Machine Translated by Google
• Rápido
• Isolado
• Repetivel
• Autoverificação
• Oportuno
Os testes unitários precisam ser rápidos, assim como foi nosso exemplo anterior. Isso é especialmente importante para TDD de
teste inicial, pois queremos feedback imediato enquanto exploramos nosso design e implementação. Se executarmos um teste
de unidade e ele levar apenas 15 segundos para ser concluído, em breve pararemos de executar testes com tanta frequência.
Degeneraremos e escreveremos grandes pedaços de código de produção sem testes, para que gastemos menos tempo
esperando a conclusão de testes lentos. Isso é exatamente o oposto do que queremos do TDD, por isso trabalhamos duro para
manter os testes rápidos. Precisamos que os testes de unidade sejam executados em 2 segundos ou menos, de preferência
milissegundos. Mesmo dois segundos é realmente um número bastante alto.
Os testes precisam ser isolados uns dos outros. Isso significa que podemos escolher qualquer teste ou combinação de
testes e executá-los na ordem que quisermos e obter sempre o mesmo resultado. Um teste não deve depender de outro
teste ter sido executado antes dele. Isso geralmente é um sintoma de falha na gravação de testes rápidos; portanto,
compensamos armazenando os resultados em cache ou organizando configurações de etapas. Isto é um erro, pois retarda
o desenvolvimento, especialmente dos nossos colegas. A razão é que não sabemos a ordem especial em que os testes
devem ser executados. Quando executamos qualquer teste por conta própria, e se não tiver sido devidamente isolado, ele
falhará como falso negativo. Esse teste não nos diz mais nada sobre o código em teste. Ele apenas nos diz que não
executamos nenhum outro teste antes dele, sem nos dizer qual teste poderia ser. O isolamento é fundamental para um fluxo
de trabalho TDD saudável.
Testes repetíveis são vitais para o TDD. Sempre que executamos um teste com o mesmo código de produção, esse teste
deve sempre retornar o mesmo resultado de aprovação ou reprovação. Isso pode parecer óbvio, mas é preciso ter cuidado
para conseguir isso. Pense em um teste que verifica uma função que retorna um número aleatório entre 1 e 10. Se afirmarmos
que o número sete foi retornado, esse teste só será aprovado ocasionalmente, mesmo que tenhamos codificado a função
corretamente. Nesse sentido, três fontes populares de sofrimento são os testes que envolvem o banco de dados, os testes
contra o tempo e os testes por meio da interface do usuário. Exploraremos técnicas para lidar com essas situações no
Capítulo 8, Test Doubles –Stubs e Mocks.
Todos os testes devem ser autoverificáveis. Isso significa que precisamos de código executável para executar e
verificar se os resultados são os esperados. Esta etapa deve ser automatizada. Não devemos deixar essa verificação
para inspeção manual, talvez gravando a saída em um console e fazendo com que um humano a verifique em um plano
de teste. Os testes unitários obtêm um enorme valor por serem automatizados. O computador verifica o código de
produção, libertando-nos do tédio de seguir um plano de teste, da lentidão das atividades humanas e da probabilidade de erro humano.
Machine Translated by Google
Testes oportunos são testes escritos no momento certo para serem mais úteis. O momento ideal para escrever um teste é antes de
escrever o código que faz o teste passar. Não é incomum ver equipes usarem abordagens menos benéficas. O pior, claro, é nunca
escrever testes unitários e confiar no controle de qualidade manual para encontrar bugs. Com essa abordagem, não recebemos
nenhum feedback de design disponível. O outro extremo é fazer com que um analista escreva todos os testes do componente – ou
mesmo de todo o sistema – antecipadamente, deixando a codificação como um exercício mecânico. Isso também não aprende com o
feedback do design. Também pode resultar em testes superespecificados que resultam em escolhas inadequadas de design e
implementação. Muitas equipes começam escrevendo algum código e depois escrevem um teste de unidade, perdendo assim a
oportunidade de feedback inicial do design.
Também pode levar a código não testado e tratamento incorreto de casos extremos.
Vimos como os PRIMEIROS princípios nos ajudam a focar na elaboração de um bom teste. Outro princípio importante é não tentar
testar muitas coisas de uma só vez. Se fizermos isso, o teste se tornará muito difícil de entender. Uma solução simples para isso é
escrever uma única afirmação por teste, que abordaremos a seguir.
Os testes fornecem feedback mais útil quando são curtos e específicos. Eles atuam como um microscópio trabalhando no código,
cada teste destacando um pequeno aspecto do nosso código. A melhor maneira de garantir que isso aconteça é escrever uma
afirmação por teste. Isso nos impede de lidar com muitas coisas em um teste. Isso se concentra nas mensagens de erro que
recebemos durante falhas de teste e nos ajuda a controlar a complexidade do nosso código. Isso nos obriga a decompor um pouco
mais as coisas.
Outro mal-entendido comum é o que uma unidade significa em um teste unitário. A unidade refere-se ao próprio isolamento do teste –
cada teste pode ser considerado uma unidade independente. Como resultado, o tamanho do código em teste pode variar muito, desde
Pensar no teste em si como uma unidade unifica várias opiniões populares sobre qual deveria ser o escopo de um teste de unidade.
Freqüentemente, diz-se que a unidade é o menor pedaço de código testável – uma função, método, classe ou pacote. Todas essas
são opções válidas. Outro argumento comum é que um teste unitário deve ser um teste de classe – uma classe de teste unitário por
classe de código de produção, com um método de teste unitário por método de produção. Embora comum, essa geralmente não é a
melhor abordagem. Ele acopla desnecessariamente a estrutura do teste à estrutura da implementação, tornando o código mais difícil
de alterar no futuro, e não mais fácil.
O objetivo ideal de um teste de unidade é cobrir um comportamento visível externamente. Isso se aplica a diversas escalas diferentes
na base de código. Podemos testar a unidade de uma história de usuário inteira em vários pacotes de classes, desde que possamos
evitar a manipulação de sistemas externos, como um banco de dados ou a interface do usuário. Veremos técnicas para fazer isso no
Capítulo 9, Arquitetura Hexagonal – Desacoplando Sistemas Externos.
Muitas vezes também utilizamos testes unitários mais próximos dos detalhes do código, testando apenas os métodos públicos de uma
única classe.
Machine Translated by Google
Depois de escrevermos nosso teste com base no design que gostaríamos que nosso código tivesse,
podemos nos concentrar no aspecto mais óbvio do teste: verificar se nosso código está correto.
• Erros pontuais
• Condições ausentes
• O algoritmo errado
Como exemplo, voltando ao nosso teste anterior para um nome de usuário em letras minúsculas, suponha que
decidimos não implementar isso usando o método .toLowerCase() integrado de String , mas em vez disso tentamos
rolar nosso próprio código de loop, assim:
Afirmando exceções 63
}
}
retornar resultado.toString() ;
}
}
Veríamos imediatamente que este código não está correto. O teste falha, conforme mostrado na figura a seguir:
O primeiro erro neste código é um erro simples de um por um – a primeira letra está faltando na saída.
Isso aponta para um erro na inicialização do nosso índice de loop, mas também existem outros erros neste código.
Este teste revela dois defeitos. Testes adicionais revelariam mais dois. Você consegue ver o que eles são apenas pela
inspeção visual? Quanto mais tempo e esforço é necessário para analisar códigos como esse em nossas cabeças, em
vez de usar testes automatizados?
Afirmando exceções
Uma área em que os testes de unidade se destacam é no teste de código de tratamento de erros. Como exemplo de teste de
lançamento de exceção, vamos adicionar um requisito comercial de que nossos nomes de usuário devem ter pelo menos quatro caracteres.
Pensamos no design que queremos e decidimos lançar uma exceção personalizada se o nome for muito curto.
Decidimos representar esta exceção personalizada como classe InvalidNameException. Esta é a
aparência do teste, usando AssertJ:
@Teste
Podemos considerar adicionar outro teste especificamente destinado a provar que um nome de quatro caracteres é aceito e
nenhuma exceção é lançada:
@Teste
assertThatNoException()
Alternativamente, podemos simplesmente decidir que este teste explícito não é necessário. Podemos cobri-lo implicitamente
com outros testes. É uma boa prática adicionar ambos os testes para deixar claras as nossas intenções.
Os nomes dos testes são bastante gerais, começando com rejeita ou aceita. Eles descrevem o resultado para o qual o código
está sendo testado. Isso nos permite mudar de ideia sobre a mecânica de tratamento de erros posteriormente, talvez mudando
para algo diferente de exceções para sinalizar o erro.
Os testes unitários podem detectar erros comuns de programação e verificar a lógica de tratamento de erros. Vejamos um
princípio importante de escrever nossos testes unitários para nos dar o máximo de flexibilidade ao implementar nossos métodos.
TDD trata de testar o comportamento dos componentes, não de suas implementações. Como vimos em nosso teste na seção
anterior, ter um teste para o comportamento que desejamos nos permite escolher qualquer implementação que faça o trabalho.
Nós nos concentramos no que é importante – o que um componente faz – e não nos detalhes menos importantes – como ele
faz isso.
Dentro de um teste, isso aparece como uma chamada de métodos ou funções públicas em classes e pacotes públicos. Os
métodos públicos são os comportamentos que escolhemos expor para uma aplicação mais ampla. Quaisquer dados privados
ou códigos de suporte em classes, métodos ou funções permanecem ocultos.
Um erro comum que os desenvolvedores cometem ao aprender TDD é tornar as coisas públicas apenas para simplificar os
testes. Resistir a tentação. Um erro típico aqui é pegar um campo de dados privado e
exponha-o para teste usando um método getter público. Isso enfraquece o encapsulamento dessa classe. Agora é mais
provável que o getter seja mal utilizado. Futuros desenvolvedores poderão adicionar métodos a outras classes que realmente
pertençam a esta. O design do nosso código de produção é importante. Felizmente, existe uma maneira simples de preservar
o encapsulamento sem comprometer os testes.
Preservando o encapsulamento
Se sentirmos que precisamos adicionar getters a todos os nossos dados privados para que o teste possa verificar se cada um está
conforme o esperado, geralmente é melhor tratar isso como um objeto de valor. Um objeto de valor é um objeto que carece de identidade.
Quaisquer dois objetos de valor que contenham os mesmos dados são considerados iguais. Usando objetos de valor,
podemos criar outro objeto contendo os mesmos dados privados e então testar se os dois objetos são iguais.
Machine Translated by Google
Em Java, isso exige que codifiquemos um método equals() personalizado para nossa classe. Se fizermos isso, também
deveremos codificar um método hashcode() , já que os dois andam de mãos dadas. Qualquer implementação que
funcione servirá. Eu recomendo usar a biblioteca Apache commons3 , que usa recursos de reflexão Java para fazer isso:
@Sobrepor
@Sobrepor
Simplesmente adicionar esses dois métodos (e a biblioteca Apache commons3 ) à nossa classe significa que podemos
manter todos os nossos campos de dados privados e ainda verificar se todos os campos contêm os dados esperados.
Simplesmente criamos um novo objeto com todos os campos esperados e afirmamos que ele é igual ao objeto que
estamos testando.
À medida que escrevemos cada teste, usamos o código em teste pela primeira vez. Isso nos permite aprender
muito sobre como nosso código é fácil de usar, permitindo-nos fazer alterações se necessário.
Nossos testes são uma rica fonte de feedback sobre nosso design. À medida que tomamos decisões, nós as escrevemos
como código de teste. Ver esse código – o primeiro uso do nosso código de produção – mostra claramente o quão bom é
o nosso design proposto. Quando nosso design não é bom, as seções AAA do nosso teste revelarão esses problemas de
design à medida que o código cheira no teste. Vamos tentar entender em detalhes como cada um deles pode ajudar a
identificar um projeto defeituoso.
Se o código em nossa etapa de organização estiver confuso, nosso objeto poderá ser difícil de criar e configurar.
Podem ser necessários muitos parâmetros em um construtor ou muitos parâmetros opcionais deixados como
nulos no teste. Pode ser que o objeto precise de muitas dependências injetadas, indicando que ele tem muitas
responsabilidades ou pode precisar de muitos parâmetros de dados primitivos para passar muitos itens de
configuração. Estes são sinais de que a forma como criamos o nosso objeto pode beneficiar de um redesenho.
Machine Translated by Google
Chamar a parte principal do código na etapa Act geralmente é simples, mas pode revelar alguns erros
básicos de design. Por exemplo, podemos ter parâmetros pouco claros que passamos, assinaturas como
uma lista de objetos Booleanos ou String . É muito difícil saber o que cada um significa. Poderíamos
redesenhar isso agrupando esses parâmetros difíceis em uma nova classe fácil de entender, chamada
objeto de configuração. Outro possível problema é se a etapa Act exigir que várias chamadas sejam feitas
em uma ordem específica. Isso é propenso a erros. É fácil ligar para eles na ordem errada ou esquecer uma
das ligações. Poderíamos redesenhar para usar um único método que englobe todos esses detalhes.
A etapa Assert revelará se os resultados do nosso código são difíceis de usar. As áreas problemáticas podem incluir a necessidade
de chamar acessadores em uma ordem específica ou talvez o retorno de alguns códigos convencionais , como uma matriz de
resultados em que cada índice tem um significado diferente. Podemos redesenhar para usar construções mais seguras em ambos
os casos.
Em cada um desses casos, uma das seções do código em nosso teste de unidade parece errada – tem um cheiro de código.
Isso ocorre porque o design do código que estamos testando tem o mesmo cheiro de código. Isso é o que significa testes de
unidade que fornecem feedback rápido sobre o design. Eles são os primeiros usuários do código que estamos escrevendo, então
podemos identificar áreas problemáticas desde o início.
Agora temos todas as técnicas necessárias para começar a escrever nosso primeiro teste para nosso aplicativo de exemplo.
Vamos começar.
Uma ideia muito importante é que um teste automatizado só pode comprovar a presença de um defeito, não a ausência. O que isto
significa é que se pensarmos numa condição limite, escrevermos um teste para ela, e o teste falhar, sabemos que temos um defeito
na nossa lógica. No entanto, se todos os nossos testes passarem, isso não significa e não pode significar que nosso código esteja
livre de defeitos. Significa apenas que nosso código está livre de todos os defeitos que pensamos em testar. Simplesmente não
existe uma solução mágica que possa garantir que nosso código esteja livre de defeitos. O TDD nos dá um impulso significativo
nessa direção, mas nunca devemos afirmar que nosso código está livre de defeitos só porque todos os nossos testes foram
aprovados. Isto é simplesmente falso.
Uma consequência importante disso é que nossos colegas engenheiros de controle de qualidade continuam tão importantes como
sempre foram, embora agora os ajudemos a começar de um ponto de vista mais fácil. Podemos entregar código testado por TDD
aos nossos colegas de controle de qualidade manual, e eles podem ter certeza de que muitos defeitos foram evitados e
comprovadamente ausentes. Isso significa que eles podem começar a trabalhar em testes exploratórios manuais, encontrando
todas as coisas que nunca pensamos em testar. Trabalhando juntos, podemos usar seus relatórios de defeitos para escrever mais
testes de unidade para retificar o que encontrarem. A contribuição dos engenheiros de controle de qualidade continua vital, mesmo
com TDD. Precisamos de toda a ajuda que nossa equipe puder obter em nossos esforços para escrever software de alta qualidade.
Machine Translated by Google
Começando Wordz 67
A cobertura de código é uma medida de quantas linhas de código foram executadas em uma determinada
execução. É medido pela instrumentação do código e isso é algo que uma ferramenta de cobertura de código fará por nós.
Geralmente é usado em conjunto com testes de unidade para medir quantas linhas de código foram executadas durante
a execução do conjunto de testes.
Em teoria, você pode ver como isso pode significar que testes ausentes podem ser descobertos de forma científica. Se
percebermos que uma linha de código não foi executada, devemos ter um teste faltando em algum lugar. Isso é verdadeiro
e útil, mas o inverso não é verdade. Suponha que obtenhamos 100% de cobertura de código durante nossa execução de
teste. Isso significa que o software agora está completamente testado e correto? Não.
Considere fazer um único teste para uma instrução if (x <2) . Podemos escrever um teste que fará com que esta
linha seja executada e incluída nos relatórios de cobertura de código. Porém, um único teste não é suficiente para
abranger todas as possibilidades de comportamento. A instrução condicional pode ter o operador errado – menor
que em vez de menor ou igual a. Pode ter o limite incorreto de 2 quando deveria ser 20. Qualquer teste único não
pode explorar completamente as combinações de comportamento nessa afirmação. Podemos fazer com que a
cobertura de código nos diga que a linha foi executada e que nosso único teste foi aprovado, mas ainda podemos
ter vários erros lógicos restantes. Podemos ter 100% de cobertura de código e ainda faltar testes.
É hora de contar uma breve história pessoal sobre como minha melhor tentativa de TDD deu espetacularmente errado. Em
um aplicativo móvel que calculava relatórios de impostos pessoais, havia uma caixa de seleção específica de sim/não no
aplicativo para indicar se você tinha um empréstimo estudantil ou não, pois isso afeta o imposto que você paga. Isso teve seis
consequências em nossa aplicação e eu testei exaustivamente cada uma delas, escrevendo cuidadosamente meus testes primeiro.
Infelizmente, eu interpretei mal a história do usuário. Eu inverti todos os testes. Onde a caixa de seleção deveria aplicar o
imposto relevante, agora não o aplicava e vice-versa.
Felizmente, isso foi percebido por nosso engenheiro de controle de qualidade. Seu único comentário foi que ela não
conseguiu encontrar absolutamente nenhuma solução alternativa no sistema para esse defeito. Concluímos que o TDD fez
um excelente trabalho ao fazer o código fazer o que eu queria, mas eu fiz um trabalho menos excelente ao descobrir o que
deveria ser. Pelo menos foi uma solução muito rápida e um novo teste.
Começando Wordz
Vamos aplicar essas ideias ao nosso aplicativo Wordz. Vamos começar com uma classe que conterá o núcleo da nossa
lógica de aplicação, uma que represente uma palavra a ser adivinhada e que possa calcular a pontuação de uma
adivinhação.
Começamos criando uma classe de teste unitário e isso imediatamente nos coloca no modo de design de
software: como devemos chamar o teste? Iremos com WordTest, pois ele descreve a área que queremos cobrir
– a palavra a ser adivinhada.
Machine Translated by Google
Estruturas típicas de projetos Java são divididas em pacotes. O código de produção reside em src/main/
java e o código de teste está localizado em src/test/java. Essa estrutura descreve como o código de produção e
de teste são partes igualmente importantes do código-fonte, ao mesmo tempo que nos fornece uma maneira de
compilar e implantar apenas o código de produção. Sempre enviamos o código de teste com o código de produção
quando lidamos com código-fonte, mas para executáveis implantados, omitimos apenas os testes. Também
seguiremos a convenção básica do pacote Java de ter um nome exclusivo para nossa empresa ou projeto no nível superior.
Isso ajuda a evitar conflitos com o código da biblioteca. Chamaremos o nosso de com.wordz, em homenagem ao aplicativo.
A próxima etapa do projeto é decidir qual comportamento eliminar e testar primeiro. Sempre queremos uma
versão simples de um caminho feliz, algo que ajude a eliminar a lógica normal que será executada com mais
frequência. Podemos cobrir casos extremos e condições de erro posteriormente. Para começar, vamos escrever
um teste que retornará a pontuação de uma única letra incorreta:
@Teste
}
}
O nome do teste nos dá uma visão geral do que o teste está fazendo.
2. Para iniciar nosso projeto, decidimos usar uma classe chamada Word para representar nossa palavra. Também
decidimos fornecer a palavra adivinhar como um parâmetro construtor para nossa instância de objeto da classe
Word que queremos criar. Codificamos estas decisões de design no teste:
@Teste
3. Usamos o preenchimento automático neste ponto para criar uma nova classe do Word em seu próprio arquivo. Check-in duplo
árvore de pastas src/main e não src/test:
Machine Translated by Google
Começando Wordz 69
6. A seguir voltamos ao teste. Capturamos o novo objeto como uma variável local para que possamos testá-lo:
@Teste
A próxima etapa do design é pensar em uma maneira de passar uma estimativa para a classe Word e retornar uma pontuação.
7. Passar o palpite é uma decisão fácil – usaremos um método que chamaremos de palpite(). Pudermos
codifique essas decisões no teste:
@Teste
palavra.adivinhar("Z");
}
Machine Translated by Google
9. Clique em Enter para adicionar o método e altere o nome do parâmetro para um nome descritivo:
10. A seguir, vamos adicionar uma maneira de obter a pontuação resultante dessa estimativa. Comece com o teste:
@Teste
Provavelmente queremos algum tipo de objeto. Este objeto deve representar a pontuação desse palpite. Como nossa
história de usuário atual é sobre as pontuações de uma palavra de cinco letras e os detalhes de cada letra, devemos
retornar uma letra exatamente certa, uma letra certa, um lugar errado ou uma letra ausente.
Existem várias maneiras de fazer isso e agora é a hora de parar e pensar sobre elas. Aqui estão algumas abordagens
viáveis:
• Uma classe com um método iterador , que itera em cinco constantes enum .
Machine Translated by Google
Começando Wordz 71
• Uma classe com um método iterador que retorna uma interface para cada pontuação de letra. O código de pontuação
implementaria uma classe concreta para cada tipo de pontuação. Esta seria uma maneira puramente orientada a
objetos de adicionar um retorno de chamada para cada resultado possível.
• Uma classe que iterou os resultados para cada letra e você passou uma função lambda do Java 8 para
cada um dos resultados. O correto seria chamado como retorno de chamada para cada letra.
Já são muitas opções de design. A parte principal do TDD é que estamos considerando isso agora, antes de
escrevermos qualquer código de produção. Para nos ajudar a decidir, vamos esboçar como será o código de chamada.
Precisamos de considerar extensões plausíveis para o código – precisaremos de mais ou menos de cinco letras
numa palavra? As regras de pontuação mudariam? Deveríamos nos preocupar com alguma dessas coisas agora ?
As pessoas que lerem este código no futuro compreenderiam mais facilmente qualquer uma dessas ideias do que
as outras? O TDD nos dá um feedback rápido sobre nossas decisões de design e isso nos obriga a fazer um treino
de design agora mesmo.
Uma decisão primordial é que não devolveremos as cores que cada letra deveria ter. Essa será uma decisão do
código da UI. Para esta lógica de domínio central, retornaremos apenas o fato de que a letra está correta, na
posição errada ou não está presente.
É bastante fácil com o TDD esboçar o código de chamada porque ele é o próprio código de teste. Após cerca de
15 minutos pensando sobre o que fazer, aqui estão as três decisões de design que usaremos neste código:
• Acessar cada pontuação por sua posição na palavra, com base em zero
Estas decisões apoiam o princípio KISS , normalmente denominado mantê-lo simples, estúpido.
A decisão de apoiar um número variável de letras me faz pensar se ultrapassei outro princípio –
YAGNI – ou se você não vai precisar dele. Neste caso, estou me convencendo de que não se
trata de um design muito especulativo e que a legibilidade do objeto da partitura compensará isso.
Vamos passar para o design:
@Teste
assertThat(resultado).isEqualTo(Letter.INCORRECT);
}
Machine Translated by Google
Podemos ver como esse teste bloqueou as decisões de design sobre como usaremos nossos objetos.
Não diz absolutamente nada sobre como implementaremos esses métodos internamente. Isso é
fundamental para um TDD eficaz. Também capturamos e documentamos todas as decisões de design neste teste.
Criar uma especificação executável como esta é um benefício importante do TDD.
2. Agora, execute este teste. Observe-o falhar. Este é um passo surpreendentemente importante.
Podemos pensar a princípio que só queremos ver passar nos testes. Isto não é totalmente verdade. Parte
do trabalho no TDD é ter confiança de que seus testes estão funcionando. Ver um teste falhar quando
sabemos que não escrevemos o código para fazê-lo passar ainda nos dá confiança de que nosso teste
provavelmente está verificando as coisas certas.
}
}
}
}
Novamente, usamos atalhos IDE para fazer a maior parte do trabalho de escrever esse código para nós. O teste passa:
Resumo 73
Podemos ver que o teste passou e demorou 0,139 segundos para ser executado. Isso certamente supera qualquer
teste manual.
Também temos um teste repetível, que podemos executar durante o restante do ciclo de vida do projeto. A economia de tempo
em comparação com o teste manual aumentará sempre que executarmos o conjunto de testes.
Você notará que, embora o teste seja aprovado, o código parece trapacear. O teste espera apenas Letter.INCORRECT e o
código é codificado para sempre retornar isso. É evidente que nunca poderia funcionar para quaisquer outros valores! Isto é
esperado nesta fase. Nosso primeiro teste estabeleceu um design aproximado para a interface do nosso código. Ainda não
começou a expulsar a implementação completa. Faremos isso em nossos testes subsequentes. Esse processo é chamado de
triangulação, onde contamos com a adição de testes para eliminar os detalhes de implementação que faltam. Ao fazer isso,
todo o nosso código é coberto por testes. Obtemos cobertura de código 100% significativa gratuitamente. Mais importante
ainda, divide o nosso trabalho em partes menores, cria progresso com resultados frequentes e pode levar a algumas soluções
interessantes.
Outra coisa a notar é que nosso único teste nos levou a criar duas classes, cobertas por aquele teste. Isto é altamente
recomendado. Lembre-se de que nosso teste unitário cobre um comportamento, não qualquer implementação específica desse
comportamento.
Resumo
Demos os primeiros passos no TDD e aprendemos sobre a estrutura AAA de cada teste. Vimos como é possível projetar nosso
software e escrever nossos testes antes do código de produção e, como resultado, obter designs mais limpos e modulares.
Aprendemos o que constitui um bom teste e aprendemos algumas técnicas comuns usadas para detectar erros comuns de
programação e testar códigos que geram exceções.
É importante entender o fluxo de uso de seções AAA em nossos FIRST testes, pois isso nos dá um modelo que podemos
seguir com segurança. Também é importante compreender o fluxo de ideias de design, conforme usado no exemplo anterior
do Wordz. Escrever nossos testes é literalmente tomar as decisões de design que tomamos e capturá-las no código de teste
unitário. Isso fornece feedback rápido sobre o quão limpo é nosso design, além de fornecer uma especificação executável para
futuros leitores de nosso código.
No próximo capítulo, adicionaremos testes e implementaremos uma implementação completa para nosso objeto de
pontuação de palavras. Veremos como o TDD tem um ritmo que impulsiona o trabalho. Usaremos o Red, Green, Refactor
abordagem para continuar refinando nosso código e manter o código e os testes limpos, sem engenharia excessiva.
Perguntas e respostas
1. Como sabemos qual teste escrever se não temos código para testar?
Nós reformulamos esse pensamento. Os testes nos ajudam a projetar uma pequena seção de código antecipadamente.
Decidimos qual interface queremos para este código e então capturamos essas decisões nas etapas AAA de um teste
de unidade. Escrevemos código apenas o suficiente para compilar o teste e, em seguida, apenas o suficiente para
fazer o teste ser executado e falhar. Neste ponto, temos uma especificação executável para nosso código para nos
guiar à medida que escrevemos o código de produção.
Machine Translated by Google
Não, e este é um mal-entendido comum ao usar testes unitários. O objetivo de cada teste é especificar e executar
um comportamento. Este comportamento será implementado de alguma forma usando código – funções, classes,
objetos, chamadas de biblioteca e similares – mas este teste não restringe de forma alguma como o comportamento
é implementado. Alguns testes unitários testam apenas uma função. Alguns têm um teste por método público por
classe. Outros, como no nosso exemplo trabalhado, dão origem a mais de uma classe para satisfazer o teste.
É uma recomendação útil começar assim, mas às vezes descobrimos que podemos omitir ou reduzir uma etapa e
melhorar a legibilidade de um teste. Poderíamos omitir o passo Arrange, se não tivéssemos nada para criar para,
digamos, um método estático. Podemos recolher a etapa Act na etapa Assert para uma chamada de método simples
para tornar o teste mais legível. Podemos fatorar nosso código de etapa comum do Arrange em um método de
anotação JUnit @BeforeEach .
Não. Eles são tratados com a mesma importância e cuidado que o código de produção. O código de teste é mantido
limpo assim como o código de produção é mantido limpo. A legibilidade do nosso código de teste é fundamental.
Devemos ser capazes de ler rapidamente um teste e ver rapidamente por que ele existe e o que faz. O código de
teste não é implantado na produção, mas isso não o torna menos importante.
Machine Translated by Google
6
Seguindo os ritmos do TDD
Vimos como os testes de unidade individuais nos ajudam a explorar e capturar decisões de design sobre nosso
código e a mantê-lo livre de defeitos e simples de usar, mas isso não é tudo que eles podem fazer. O TDD possui
ritmos que nos auxiliam em todo o ciclo de desenvolvimento. Seguindo os ritmos, temos um guia sobre o que fazer
a seguir em cada etapa. É útil ter essa estrutura técnica que nos permite pensar profundamente sobre a engenharia
de um bom código e então capturar os resultados.
O primeiro ritmo foi abordado no último capítulo. Dentro de cada teste, temos um ritmo de escrita das
seções Arrange, Act e Assert. Adicionaremos algumas observações detalhadas sobre o sucesso disso a seguir.
Continuaremos abordando o ritmo mais amplo que nos orienta à medida que refinamos nosso código, conhecido como
ciclo de refatoração vermelho, verde (RGR) . Juntos, eles nos ajudam a criar nosso código para ser fácil de integrar na
aplicação mais ampla e feito de código limpo e simples de entender. A aplicação desses dois ritmos garante a entrega de
código de alta qualidade no ritmo certo. Ele nos fornece vários pequenos marcos a serem atingidos durante cada sessão
de codificação. Isso é altamente motivador, pois ganhamos uma sensação de progresso constante em direção ao nosso
objetivo de construir nosso aplicativo.
Requerimentos técnicos
O código final deste capítulo pode ser encontrado em https://github.com/PacktPublishing/
Desenvolvimento orientado a testes com Java/tree/main/chapter06. Recomenda-se acompanhar
o exercício digitando você mesmo o código – e pensando em todas as decisões que tomaremos
à medida que avançamos.
teste. Isso nos força a projetar como nosso código será usado – a parte externa do nosso código. Se
pensarmos em um objeto como sendo um limite de encapsulamento, faz sentido falar sobre o que está
dentro e fora desse limite. Os métodos públicos formam a parte externa do nosso objeto. O ritmo Arrange,
Act and Assert nos ajuda a projetá-los.
Estamos usando a palavra ritmo aqui num sentido quase musical. É um tema constante e repetido que mantém nosso
trabalho unido. Há um fluxo regular de trabalho para escrever testes, escrever código, melhorar esse código e, em seguida,
decidir qual teste escrever a seguir. Cada teste e cada trecho de código serão diferentes, mas o ritmo de trabalho é o
mesmo, como se fosse uma batida constante em uma música em constante mudança.
Depois de escrever nosso teste, passamos a criar o código que está dentro de nosso objeto – os campos e métodos
privados. Para isso, utilizamos outro ritmo chamado RGR. Este é um processo de três etapas que nos ajuda a
aumentar a confiança em nosso teste, criar uma implementação básica de nosso código e depois refiná-lo com segurança.
Nesta seção, aprenderemos que trabalho precisa ser feito em cada uma das três fases.
Começando no vermelho
Sempre começamos com a primeira fase chamada fase vermelha. O objetivo desta fase é usar o modelo Arrange, Act and
Assert para colocar nosso teste em funcionamento e pronto para testar o código que escreveremos a seguir.
A parte mais importante desta fase é garantir que o teste não seja aprovado. Chamamos isso de teste com falha, ou teste
vermelho, devido à cor que a maioria das ferramentas de teste gráfico usa para indicar um teste com falha.
Isso é bastante contra-intuitivo, não é? Normalmente, pretendemos fazer com que as coisas funcionem bem na primeira
vez no desenvolvimento. No entanto, queremos que nosso teste falhe neste estágio para nos dar a confiança de que está
funcionando corretamente. Se o teste passar neste ponto, é uma preocupação. Por que isso passa? Sabemos que ainda
não escrevemos nenhum código que estamos testando. Se o teste passar agora, isso significa que não precisamos escrever
nenhum código novo ou cometemos um erro no teste. A seção Leitura adicional contém um link para oito motivos pelos
quais um teste pode não estar sendo executado corretamente.
O erro mais comum aqui é errar na afirmação. Identifique o erro e corrija-o antes de prosseguir.
Devemos ter esse teste vermelho em vigor para que possamos vê-lo mudar de reprovação para aprovação
à medida que adicionamos o código corretamente.
Machine Translated by Google
Assim que tivermos nosso teste reprovado, estaremos livres para escrever o código que o fará passar. Chamamos
isso de código de produção – o código que fará parte do nosso sistema de produção. Tratamos nosso código de
produção como um componente de caixa preta. Pense em um circuito integrado em eletrônica, ou talvez em algum
tipo de unidade mecânica selada. O componente tem um interior e um exterior. O interior é onde escrevemos nosso
código de produção. É onde escondemos os dados e algoritmos da nossa implementação. Podemos fazer isso
usando qualquer abordagem que escolhermos – orientada a objetos, funcional, declarativa ou processual. Qualquer coisa
nós imaginamos. A parte externa é a Interface de Programação de Aplicativo (API). Esta é a parte que usamos para
nos conectar ao nosso componente e usá-lo para construir softwares maiores. Se escolhermos uma abordagem
orientada a objetos, esta API será composta por métodos públicos em um objeto. Com o TDD, a primeira peça à qual
nos conectamos é o nosso teste, e isso nos dá um feedback rápido sobre a facilidade de uso da conexão.
O diagrama a seguir mostra as diferentes partes – o interior, o exterior, o código de teste e outros usuários do
nosso componente:
Como nossa implementação é encapsulada, podemos mudar de ideia mais tarde, à medida que aprendemos
mais, sem interromper o teste.
• Use o código mais simples que possa funcionar: É importante usar o código mais simples. Pode haver a
tentação de usar algoritmos excessivamente projetados ou talvez usar o recurso de linguagem mais recente
apenas como uma desculpa para usá-lo. Resista a esta tentação. Nesta fase, nosso objetivo é fazer passar no
teste e nada mais.
• Não pense demais nos detalhes da implementação: Não precisamos pensar demais nisso. Não precisamos
escrever o código perfeito na primeira tentativa. Podemos escrever uma única linha, um método, vários métodos
ou classes inteiramente novas. Melhoraremos esse código na próxima etapa. Apenas lembre-se de fazer o teste
passar e não ir além do que este teste cobre em termos de funcionalidade.
Esta é a fase em que entramos no modo de engenharia de software. Temos um código simples e funcional com um
teste aprovado. Agora é a hora de refinar isso em um código limpo – ou seja, um código que será fácil de ler mais tarde.
Com a confiança que um teste aprovado proporciona, somos livres para aplicar qualquer técnica de refatoração válida
ao nosso código. Alguns exemplos de técnicas de refatoração que podemos usar durante esta fase incluem o seguinte:
Todas essas técnicas têm um objetivo: tornar nosso código mais fácil de entender. Isso tornará mais fácil a
manutenção. Lembre-se de manter o teste verde passando durante essas mudanças. Ao final desta fase, teremos
um teste de unidade cobrindo uma parte do código de produção que projetamos para ser fácil de trabalhar no
futuro. Esse é um bom lugar para estar.
Agora que estamos familiarizados com o que fazer em cada fase do ciclo RGR, vamos aplicar isso ao nosso aplicativo Wordz.
Então, o que devemos escrever para nossos próximos testes? Qual seria um passo útil e pequeno o suficiente
para não cairmos na armadilha de escrever além do que nossos testes podem suportar? Nesta seção,
continuaremos construindo o sistema de pontuação de aplicativos Wordz usando TDD. Discutiremos como
escolhemos avançar em cada etapa.
Para o próximo teste, uma boa escolha é jogar pelo seguro e avançar apenas um pequeno passo. Adicionaremos
um teste para uma única letra correta. Isso eliminará nossa primeira lógica de aplicação genuína:
1. Vamos começar no vermelho. Escreva um teste com falha para uma única letra correta:
@Teste
assertThat(pontuação.letter(0))
.isEqualTo(Letra.CORRETO);
}
Este teste é intencionalmente semelhante ao anterior. A diferença é que ele testa se uma letra está correta,
em vez de incorreta. Usamos a mesma palavra – uma única letra, "A"
– intencionalmente. Isso é importante ao escrever testes – use dados de teste que ajudem a contar a história do que
estamos testando e por quê. A história aqui é que a mesma palavra com uma estimativa diferente levará a uma
pontuação diferente – obviamente a chave para o problema que estamos resolvendo. Nossos dois casos de teste
cobrem completamente os dois resultados possíveis de qualquer suposição de uma palavra de uma única letra.
Usando nossos recursos de preenchimento automático do IDE, chegamos rapidamente às alterações na classe Word.
2. Agora vamos passar para o verde adicionando o código de produção para fazer o teste passar:
pontuação.avaliar(0, tentativa);
pontuação de retorno;
}
}
O objetivo aqui é fazer com que o novo teste seja aprovado e, ao mesmo tempo, manter a aprovação no
teste existente. Não queremos quebrar nenhum código existente. Adicionamos um campo chamado word,
que armazenará a palavra que deveríamos adivinhar. Adicionamos um construtor público para inicializar
este campo. Adicionamos código ao método guess() para criar um novo objeto Score . Decidimos
adicionar um método a esta classe Score chamadoavali (). Este método tem a responsabilidade de avaliar
qual deve ser a pontuação do nosso palpite. Decidimos queassess() deveria ter dois parâmetros. O
primeiro parâmetro é um índice baseado em zero para qual letra da palavra desejamos avaliar uma
pontuação. O segundo parâmetro é o nosso palpite sobre qual poderia ser a palavra.
}
}
Para cobrir o novo comportamento testado pelo teste oneCorrectLetter() , adicionamos o código
anterior. Em vez do método avaliar() sempre retornar Letter.INCORRECT como fazia
anteriormente, o novo teste forçou uma nova direção. O método avaliar() agora deve ser capaz
de retornar a pontuação correta quando uma letra adivinhada estiver correta.
Para conseguir isso, adicionamos um campo chamado result para armazenar a pontuação mais recente,
código para retornar esse resultado do método letter() e código no método assessment() para verificar se
a primeira letra do nosso palpite corresponde à primeira letra do nossa palavra. Se acertarmos, ambos os
nossos testes deverão passar agora.
Há muito o que revisar aqui. Observe como nossos dois testes estão passando. Ao executar todos os testes
até agora, comprovamos que não quebramos nada. As alterações que fizemos em nosso código adicionaram
o novo recurso e não quebraram nenhum recurso existente. Isso é poderoso. Observe outro aspecto óbvio –
sabemos que nosso código funciona. Não precisamos esperar até uma fase de teste manual, esperar até
algum ponto de integração ou esperar até que a interface do usuário esteja pronta. Sabemos que nosso
código funciona agora. Como um ponto menor, observe a duração de 0,103 segundos. Os dois testes foram
concluídos em um décimo de segundo, muito mais rápido do que testar manualmente. Nada mal.
Agora que o teste foi aprovado, podemos seguir em frente – mas uma parte importante do TDD é melhorar
continuamente nosso código e trabalhar em direção a um design melhor, guiado por testes. Entramos agora
na fase de refatoração do ciclo RGR. Mais uma vez, o TDD nos devolve o controle. Queremos refatorar? Que
coisas devemos refatorar? Por que? Vale a pena fazer isso agora ou podemos adiar para uma etapa posterior?
Machine Translated by Google
Vamos revisar o código e procurar por cheiros de código. Um cheiro de código é uma indicação de que a implementação
pode precisar de melhorias. O nome vem da ideia do cheiro que a comida sente quando começa
a explodir.
Um cheiro de código é código duplicado. Sozinho, um pouco de código duplicado pode funcionar. Mas é um
aviso prévio de que talvez tenha sido utilizado demasiado recurso de copiar e colar e que não conseguimos
captar um conceito importante de forma mais direta. Vamos revisar nosso código para eliminar a duplicação.
Também podemos procurar outros dois cheiros de código comuns – nomenclatura pouco clara e
blocos de código que seriam mais fáceis de ler se fossem extraídos em seu próprio método.
Obviamente, isto é subjetivo e todos teremos opiniões diferentes sobre o que mudar.
O termo cheiro de código apareceu originalmente no wiki C2. Vale a pena ler para ver os exemplos
dados de cheiros de código. Ele tem uma definição útil que indica que um cheiro de código é algo
que precisa ser revisado, mas pode não necessariamente
precisar ser alterado: https://wiki.c2.com/?CodeSmell.
Vamos refletir sobre o interior do métodoassess() . Parece confuso com muito código. Vamos extrair
um método auxiliar para adicionar alguma clareza. Sempre podemos reverter a mudança se acharmos
que ela não ajuda.
Mais uma vez, executamos todos os testes para provar que esta refatoração não quebrou nada. Os
testes passam. No código anterior, dividimos uma instrução condicional complexa em seu próprio
método privado. A motivação era colocar um nome de método no código. Esta é uma forma eficaz de
comentar nosso código – de uma forma que o compilador nos ajude a nos manter atualizados. Isso
ajuda o código de chamada no método Assessment() a contar uma história melhor. A declaração if agora
diz “se esta for uma letra correta” mais ou menos em inglês. Essa é uma ajuda poderosa para a legibilidade.
Machine Translated by Google
Esta é uma pergunta válida, pois qualquer linha de código será lida por programadores humanos muito mais
vezes do que foi escrita. A legibilidade é ganha ou perdida quando você escreve o código. Qualquer linha de
código pode ser escrita para ser fácil ou difícil de ler. Podemos escolher como escritores. Se escolhermos
consistentemente a facilidade de leitura em detrimento de qualquer outra coisa, outros acharão nosso código fácil de ler.
Há mais duas áreas que quero refatorar neste estágio. O primeiro é um método simples para melhorar a
legibilidade do teste.
Vamos refatorar o código de teste para melhorar sua clareza. Adicionaremos um método assert personalizado :
@Teste
assertScoreForLetter(pontuação, 0, Letter.CORRECT);
}
O código anterior pegou a afirmação assertThat() e a moveu para seu próprio método privado.
Chamamos esse método de assertScoreForLetter() e atribuímos a ele uma assinatura que descreve
quais informações são necessárias. Essa alteração fornece uma descrição mais direta do que o teste
está fazendo, ao mesmo tempo que reduz alguns códigos duplicados. Também nos protege contra
mudanças na implementação da afirmação. Este parece ser um passo em direção a uma afirmação
mais abrangente, da qual precisaremos quando apoiarmos as suposições com mais letras. Mais uma
vez, em vez de adicionar um comentário ao código-fonte, usamos um nome de método para capturar
a intenção do código assertThat() . Escrever matchers personalizados AssertJ é outra maneira de fazer isso.
A próxima refatoração que podemos querer fazer é um pouco mais controversa, pois é uma mudança de design.
Vamos refatorar, discutir e possivelmente reverter o código se não gostarmos dele. Isso economizará
horas de reflexão sobre como seria a mudança.
Machine Translated by Google
4. Vamos mudar a forma como especificamos a posição da letra a ser verificada no método Assessment() :
A razão pela qual esta mudança é controversa é que ela exige que alteremos o código de teste para
refletir essa mudança na assinatura do método. Estou preparado para aceitar isso, sabendo que posso
usar meu suporte à refatoração automatizada do IDE para fazer isso com segurança. Isso também
introduz um risco: devemos garantir que a posição esteja definida com o valor correto antes de chamar
isCorrectLetter(). Veremos como isso se desenvolve. Isso pode tornar o código mais difícil de entender;
nesse caso, o método avaliar() simplificado provavelmente não valerá a pena. Podemos mudar a nossa
abordagem se considerarmos que este é o caso.
Machine Translated by Google
Chegamos agora a um ponto em que o código está completo para qualquer palavra de uma única letra. O
que devemos tentar a seguir? Parece que deveríamos passar para palavras de duas letras e ver como isso
muda nossos testes e nossa lógica.
Podemos prosseguir adicionando testes com o objetivo de fazer com que o código lide com combinações de duas
letras. Esta é uma etapa óbvia a ser executada depois de fazer o código funcionar com uma única letra. Para fazer
isso, precisaremos introduzir um novo conceito no código: uma letra pode estar presente na palavra, mas não na
posição que imaginamos :
1. Vamos começar escrevendo um teste para uma segunda letra que está na posição errada:
@Teste
void segundaLetraWrongPosition() {
var palavra = nova palavra("AR");
var pontuação = word.guess("ZA");
assertScoreForLetter(pontuação, 1,
Letra.PART_CORRECT);
}
Vamos alterar o código dentro do método Assessment() para fazer isso passar e manter os testes
existentes passando.
2. Vamos adicionar o código inicial para verificar todas as letras do nosso palpite:
A principal mudança aqui é avaliar todas as letras na tentativa e não presumir que há apenas
uma letra. Esse, claro, foi o objetivo deste teste – eliminar esse comportamento. Ao optar por
converter a string de tentativa em um array de char, o código parece ler muito bem. Este
algoritmo simples itera sobre cada caractere, usando a variável atual para representar o valor atual.
Machine Translated by Google
carta a ser avaliada. Isso requer que o método isCorrectLetter() seja refatorado
para aceitar e funcionar com a entrada char – bem, isso ou converter char em
String, e isso parece feio.
Os testes originais para comportamentos de uma única letra ainda passam, como deveriam. Sabemos que a lógica
dentro do nosso loop não pode estar correta – estamos simplesmente sobrescrevendo o campo de resultado , que
só pode armazenar o resultado para uma letra, no máximo. Precisamos melhorar essa lógica, mas não faremos
isso até adicionarmos um teste para isso. Trabalhar dessa forma é conhecido como triangulação – tornamos o
código de uso mais geral à medida que adicionamos testes mais específicos. Para nossa próxima etapa,
adicionaremos código para detectar quando nossa tentativa de letra ocorre na palavra em alguma outra posição.
3. Vamos adicionar código para detectar quando uma letra correta está na posição errada:
Adicionamos uma chamada para um novo método privado, ocorreInWord(), que retornará
verdadeiro se a letra atual ocorrer em qualquer lugar da palavra. Já estabelecemos que esta carta
atual não está no lugar certo. Isso deve nos dar um resultado claro para uma letra correta que não
está na posição correta.
Este código faz com que todos os três testes sejam aprovados. Imediatamente, isso é suspeito, pois não deveria acontecer.
Já sabemos que nossa lógica substitui o campo de resultado único e isso significa que muitas
combinações falharão. O que aconteceu é que o nosso último teste é bastante fraco. Poderíamos
voltar atrás e reforçar esse teste, acrescentando uma afirmação extra. Alternativamente, podemos
deixar como está e escrever outro teste. Dilemas como esse são comuns no desenvolvimento e
geralmente não vale a pena gastar muito tempo pensando neles. De qualquer forma, nos moverá adiante.
Vamos adicionar outro teste para exercitar completamente o comportamento em torno da segunda letra
estar na posição errada.
Machine Translated by Google
@Teste
void allScoreCombinations() {
var palavra = new Palavra("ARI");
var pontuação = word.guess("ZAI");
assertScoreForLetter(pontuação, 0, Letter.INCORRECT);
assertScoreForLetter(pontuação, 1,
Letra.PART_CORRECT);
assertScoreForLetter(pontuação, 2, Letter.CORRECT);
}
Como esperado, este teste falha. O motivo é óbvio ao inspecionar o código de produção. É porque
estávamos armazenando resultados no mesmo campo de valor único. Agora que temos um teste que
falhou, podemos corrigir a lógica de pontuação.
5. Adicione uma lista de resultados para armazenar o resultado de cada posição de letra separadamente:
resultados.add(Letra.INCORRETO);
posição++;
}
}
correto.contains(String.valueOf(atual));
}
}
}
Foram necessárias algumas tentativas para acertar, devido a falhas no teste que acabamos de adicionar.
O resultado final anterior passa em todos os quatro testes, provando que pode pontuar corretamente
todas as combinações em uma palavra de três letras. A principal mudança foi substituir o campo de
resultado de valor único por um ArrayList de resultados e alterar o método de implementação
letter(position) para usar esta nova coleção de resultados. A execução dessa alteração causou uma
falha, pois o código não conseguia mais detectar uma letra incorreta. Anteriormente, isso era tratado
pelo valor padrão do campo de resultado . Agora, devemos fazer isso explicitamente para cada letra.
Precisamos então atualizar a posição dentro do loop para rastrear qual posição da letra estamos avaliando.
Adicionamos um teste, vimos ele ficar vermelho e falhar, depois adicionamos código para fazer o teste
ficar verde e passar, então agora é hora de refatorar. Há coisas sobre o teste e o código de produção
que não parecem muito certas.
posição++;
}
}
if (ocorre no Word(atual)) {
retornar Letra.PART_CORRECT;
}
retornar Carta.INCORRETO;
Isso pode ser lido com muito mais clareza. O corpo do método scoreFor() agora é uma descrição
concisa das regras para pontuar cada letra. Substituímos a construção if-else-if por uma
construção if-return mais simples . Calculamos qual é a pontuação e saímos do método imediatamente.
A próxima tarefa é limpar o código de teste. No TDD, o código de teste recebe prioridade igual ao
código de produção. Faz parte da documentação do sistema. Ele precisa ser mantido e estendido
junto com o código de produção. Tratamos a legibilidade do código de teste com a mesma importância
que o código de produção.
O cheiro do código com o código de teste está em torno das afirmações. Duas coisas poderiam ser
melhoradas. Há uma duplicação óbvia no código que poderíamos eliminar. Há também uma questão
sobre quantas afirmações devem ser feitas em um teste.
@Teste
void allScoreCombinations() {
var palavra = new Palavra("ARI");
var pontuação = word.guess("ZAI");
assertScoreForGuess(pontuação, INCORRETO,
Machine Translated by Google
PART_CORRETO,
CORRETO);
assertThat(pontuação.letra(posição))
.isEqualTo(esperado);
}
}
Os testes anteriores agora podem ser modificados manualmente para fazer uso deste novo auxiliar de
asserção. Isso nos permite incorporar o método assertScoreForLetter() original , pois ele não agrega mais valor.
8. Agora, vamos dar uma olhada no conjunto final de testes após nossa refatoração:
pacote com.wordz.domain;
importar org.junit.jupiter.api.Test;
importar estático com.wordz.domain.Letter.*; importar estático
org.assertj.core.api.Assertions.assertThat;
@Teste
Machine Translated by Google
@Teste
@Teste
}
}
}
Machine Translated by Google
Este parece ser um conjunto abrangente de casos de teste. Cada linha de código de produção foi
eliminada como resultado direto da adição de um novo teste para explorar um novo aspecto do
comportamento. O código de teste parece fácil de ler e o código de produção também parece claramente
implementado e simples de chamar. O teste forma uma especificação executável das regras para adivinhar uma palavra.
Isso alcançou tudo o que pretendemos no início desta sessão de codificação. Aumentamos a
capacidade de nossa classe Score usando TDD. Seguimos o ciclo RGR para manter nosso código
de teste e código de produção seguindo as boas práticas de engenharia. Temos um código robusto,
validado por testes unitários, e um design que facilita a chamada desse código em nossa aplicação mais ampla.
Resumo
Neste capítulo, aplicamos o ciclo RGR ao nosso código. Vimos como isso divide o trabalho em tarefas separadas, o que
resulta em confiança em nosso teste, um caminho rápido para um código de produção simples e menos tempo gasto para
melhorar a capacidade de manutenção de nosso código. Analisamos a remoção de cheiros de código tanto do código de
produção quanto do código de teste. Como parte do nosso trabalho neste capítulo, usamos ideias que nos ajudam a seguir
em frente e decidir quais testes devemos escrever em seguida. As técnicas neste capítulo nos permitem escrever vários
testes e eliminar de forma incremental a lógica detalhada em nosso código de produção.
No próximo capítulo, aprenderemos sobre algumas ideias de design orientado a objetos conhecidas como princípios
SOLID, que nos permitem usar o TDD para expandir ainda mais nossa aplicação.
Perguntas e respostas
1. Quais são os dois ritmos principais do TDD?
Organizar, agir, afirmar e RGR. O primeiro ritmo nos ajuda a escrever o corpo do teste enquanto projetamos a
interface para nosso código de produção. O segundo ritmo funciona para nos ajudar a criar e refinar a
implementação desse código de produção.
Em vez de pensar em como implementaremos algum código, pensamos em como chamaremos esse código.
Capturamos essas decisões de design dentro de um teste unitário.
Não. No TDD, os testes unitários recebem peso igual ao código de produção. Eles são escritos com o mesmo
cuidado e armazenados no mesmo repositório de código. A única diferença é que o código de teste em si não
estará presente no executável entregue.
Não. Use esse tempo como uma oportunidade para decidir qual refatoração é necessária. Isso se aplica tanto ao
código de produção quanto ao código de teste. Às vezes, nada é necessário e seguimos em frente. Outras vezes,
sentimos que uma mudança maior seria benéfica. Podemos optar por adiar essa mudança maior para mais tarde,
quando tivermos mais código em vigor.
Machine Translated by Google
Leitura adicional 93
Leitura adicional
Um artigo de Jeff Langr que descreve oito maneiras diferentes pelas quais um teste pode ser aprovado pelos motivos errados.
Se estivermos cientes desses problemas, poderemos evitá-los enquanto trabalhamos.
https://medium.com/pragmatic-programmers/3-5-getting-green-on
vermelho-d189240b1c87
O guia definitivo para refatorar código. O livro descreve transformações passo a passo de código que preservam
seu comportamento, mas melhoram a clareza. Curiosamente, a maioria das transformações vem em pares,
como o par de técnicas conhecidas como Método de Extração e Método Inline. Isso reflete as compensações
envolvidas.
Este capítulo mencionou brevemente os matchers personalizados AssertJ. Estas são formas muito úteis de
criar asserções personalizadas reutilizáveis para o seu código. Essas classes de asserção são testáveis em
unidade e podem ser escritas usando TDD test-first. Só por esse motivo, eles são superiores a adicionar um
método privado para lidar com uma afirmação personalizada.
O link a seguir fornece muitos exemplos fornecidos pela distribuição AssertJ no github.
https://github.com/assertj/assertj-examples/tree/main/assertions exemplos/src/test/java/org/assertj/examples/
custom
Machine Translated by Google
Machine Translated by Google
7
Projeto de condução –
TDD e SOLID
Até agora, criamos alguns testes unitários básicos que geraram um design simples para algumas classes.
Experimentamos como o desenvolvimento orientado a testes (TDD) torna central a tomada de decisões
sobre escolhas de design. Para desenvolver uma aplicação maior, precisaremos ser capazes de lidar
com projetos de maior complexidade. Para fazer isso, aplicaremos algumas abordagens recomendadas
para avaliar o que torna um projeto preferível a outro.
Os princípios SOLID são cinco diretrizes de projeto que orientam os projetos para serem mais flexíveis e
modulares. A palavra SOLID é um acrônimo, onde cada letra representa um dos cinco princípios cujos nomes
começam com aquela letra. Esses princípios já existiam muito antes de serem conhecidos por esse nome.
Eles se mostraram úteis em minha experiência e vale a pena entender os benefícios que cada um traz e como
podemos aplicá-los ao nosso código. Para fazer isso, usaremos um exemplo de código em execução neste capítulo.
É um programa simples que desenha formas de vários tipos usando arte simples do American Standard Code
for Information Interchange (ASCII) em um console.
Antes de começarmos, vamos pensar na melhor forma de aprender esses cinco princípios. A sigla SOLID é fácil
de dizer, mas não é a maneira mais fácil de aprender os princípios. Alguns princípios baseiam-se em outros. A
experiência mostra que alguns são mais usados do que outros, especialmente quando se faz TDD. Por esse
motivo, revisaremos os princípios da ordem SDLOI. Não parece tão bom, como tenho certeza que você
concordará, mas é uma ordem de aprendizagem melhor.
Originalmente, os princípios SOLID foram concebidos como padrões aplicados a classes de programação
orientada a objetos (OOP), mas são de uso mais geral do que isso. Eles se aplicam igualmente a métodos
individuais em uma classe, bem como à própria classe. Eles também se aplicam ao projeto de interconexões
de microsserviços e ao projeto de funções na programação funcional. Veremos exemplos aplicados tanto no
nível da classe quanto no nível do método neste capítulo.
Machine Translated by Google
Requerimentos técnicos
O código deste capítulo pode ser encontrado em https://github.com/PacktPublishing/Test Driven-
Development-with-Java/tree/main/chapter07. É fornecido um exemplo de execução de código que desenha
formas usando todos os cinco princípios SOLID.
@Teste
assertThat(pontuação.letter(0)).isEqualTo(Letter.INCORRECT);
}
Decidimos o seguinte:
• O que testar
Todas essas são decisões de design que nossas mentes humanas devem tomar. O TDD nos deixa muito práticos
quando se trata de projetar nosso código e decidir como ele deve ser implementado. Para ser honesto, estou feliz
com isso. Projetar é gratificante e o TDD fornece uma estrutura útil em vez de uma abordagem prescritiva. O TDD
atua como um guia para nos lembrar de tomar essas decisões de design antecipadamente. Ele também fornece
uma maneira de documentar essas decisões como código de teste. Nada mais, mas igualmente, nada menos.
Pode ser útil usar técnicas como programação em pares ou mobbing (também conhecido como programação em
conjunto) ao tomarmos essas decisões – então, adicionamos mais experiência e mais ideias à nossa solução.
Trabalhando sozinhos, temos simplesmente que tomar as melhores decisões possíveis, com base na nossa própria experiência.
O ponto crítico a ser explicado aqui é que o TDD não toma e não pode tomar essas decisões por nós.
Devemos fazê-los. Como tal, é útil ter algumas diretrizes para nos orientar em direção a melhores designs.
Um conjunto de cinco princípios de design conhecidos como princípios SOLID é útil. SOLID é um acrônimo
para os cinco princípios a seguir:
• PRS
• OCP
• PSL
• ISP
• MERGULHAR
Nas seções a seguir, aprenderemos o que são esses princípios e como eles nos ajudam a escrever códigos e
testes bem projetados. Começaremos com o SRP, que é sem dúvida o princípio mais fundamental de qualquer
estilo de design de programa.
Este diagrama mostra uma visão geral do código Java disponível na pasta GitHub deste capítulo.
Usaremos partes específicas do código para ilustrar como cada um dos princípios SOLID foi usado
para criar esse design.
Diagramas UML
A UML foi criada em 1995 por Grady Booch, Ivar Jacobson e James Rumbaugh. UML é uma forma de visualizar projetos
OO em alto nível. O diagrama anterior é um diagrama de classes UML. A UML oferece muitos outros tipos de diagramas
úteis. Você pode aprender mais em https://
www.packtpub.com/product/uml-2-0-in-action-a-project-based tutorial/9781904811558.
O SRP nos orienta na divisão do código em partes que encapsulam um único aspecto de nossa solução. Talvez seja um
aspecto técnico por natureza – como a leitura de uma tabela de banco de dados – ou talvez seja uma regra de negócios.
De qualquer forma, dividimos diferentes aspectos em diferentes partes de código. Cada trecho de código é responsável
por um único detalhe, daí vem o nome SRP. Outra maneira de ver isso é que um trecho de código só deve ter um motivo
para ser alterado. Vamos examinar por que isso é uma vantagem nas seções a seguir.
Machine Translated by Google
O bloco A trata de três coisas, portanto uma alteração em qualquer uma delas implica uma alteração em A. Para
melhorar isso, aplicamos o SRP e separamos o código responsável pela criação do HTML, aplicação das regras
de negócio e acesso ao banco de dados. Cada um desses três blocos de código – A, B e C – agora tem apenas
um motivo para mudar. A alteração de qualquer bloco de código único não deve resultar em alterações nos outros blocos.
Cada bloco de código trata de uma coisa e tem apenas um motivo para mudar. Podemos ver que o SRP funciona para
limitar o escopo de futuras alterações de código. Também torna mais fácil encontrar código em uma grande base de código,
pois é organizado de forma lógica.
A reutilização de código tem sido um objetivo da engenharia de software há muito tempo. Criar software do zero leva tempo, custa
dinheiro e impede que um engenheiro de software faça outra coisa. Faz sentido que, se criarmos algo que seja geralmente útil, o
utilizemos novamente sempre que possível. A barreira para isso acontece quando criamos grandes softwares específicos para aplicativos.
O facto de serem altamente especializados significa que só podem ser utilizados no seu contexto original.
Ao criar componentes de software menores e de uso mais geral, seremos capazes de usá-los novamente em diferentes contextos.
Quanto menor o escopo do que o componente pretende fazer, maior será a probabilidade de podermos reutilizá-lo sem modificação. Se
tivermos uma pequena função ou classe que faz alguma coisa, fica fácil reutilizá-la em nossa base de código. Pode até acabar como
parte de uma estrutura ou biblioteca que podemos reutilizar em vários projetos.
O SRP não garante que o código será reutilizável, mas visa reduzir o escopo de qualquer parte do código. Essa maneira de pensar no
código como uma série de blocos de construção onde cada um realiza uma pequena parte da tarefa geral tem maior probabilidade de
resultar em componentes reutilizáveis.
À medida que escrevemos código, estamos cientes de que não estamos apenas escrevendo para resolver um problema agora, mas
também escrevendo código que poderá ser revisitado no futuro. Isso pode ser feito por outras pessoas da equipe ou talvez por nós
mesmos. Queremos tornar este trabalho futuro o mais simples possível. Para conseguir isso, precisamos manter nosso código bem
projetado, tornando-o seguro e fácil de trabalhar posteriormente.
g.drawLine(0, r.getWidth());
}
}
}
}
}
Podemos perceber que este código tem quatro responsabilidades, sendo elas:
detalhes de implementação para desenhar cada tipo de forma nas instruções case
Machine Translated by Google
Se quisermos adicionar um novo tipo de forma – triângulo, por exemplo – precisaremos alterar este código.
Isso o tornará mais longo, pois precisamos adicionar detalhes sobre como desenhar a forma dentro de uma nova
instrução case . Isso torna o código mais difícil de ler. A turma também terá que fazer novas provas.
Podemos alterar este código para facilitar a adição de um novo tipo de forma? Certamente. Vamos aplicar o
SRP e refatorar.
formatos de embalagens;
}
}
}
}
Machine Translated by Google
O código que costumava estar nos blocos de instrução case foi movido para as classes de forma.
Vejamos as mudanças na classe Rectangle como um exemplo – você pode ver o que mudou no
seguinte trecho de código:
Podemos ver como a classe Rectangle agora tem a responsabilidade única de saber como desenhar um
retângulo. Não faz mais nada. A única razão pela qual isso terá que mudar é se precisarmos mudar a forma
como um retângulo é desenhado. Isto é improvável, o que significa que agora temos uma abstração estável. Em
outras palavras, a classe Rectangle é um bloco de construção no qual podemos confiar. É improvável que mude.
Se examinarmos nossa classe Shapes refatorada , veremos que ela também melhorou. Ele tem uma
responsabilidade a menos porque transferimos isso para as classes TextBox e Rectangle . Já é mais
simples de ler e mais simples de testar.
PRS
Faça uma coisa e faça bem. Tenha apenas um motivo para a alteração de um bloco de código.
Mais melhorias podem ser feitas. Vemos que a classe Shapes retém sua instrução switch e que cada
instrução case parece duplicada. Todos eles fazem a mesma coisa, que é chamar draw ()
método em uma classe de forma. Podemos melhorar isso substituindo totalmente a instrução switch – mas
isso terá que esperar até a próxima seção, onde apresentaremos o DIP.
Antes de fazermos isso, vamos pensar em como o SRP se aplica ao nosso próprio código de teste.
Machine Translated by Google
O SRP também nos ajuda a organizar nossos testes. Cada teste deve testar apenas uma coisa. Talvez este fosse
um único caminho feliz ou uma única condição limite. Isso torna mais simples localizar quaisquer falhas.
Encontramos o teste que falhou e, como se trata apenas de um único aspecto do nosso código, é fácil encontrar
o código onde o defeito deve estar. A recomendação de ter apenas uma asserção para cada teste decorre
naturalmente disso.
Este é um exemplo de aplicação de SRP a cada configuração desse grupo de objetos e de captura disso
escrevendo um teste para cada configuração específica.
Vimos como o SRP nos ajuda a criar blocos de construção simples para nosso código, que são mais simples de testar e
mais fáceis de trabalhar. O próximo princípio SOLID poderoso a ser observado é o DIP. Esta é uma ferramenta muito
poderosa para gerenciar a complexidade.
Nesta seção, aprenderemos como o DIP nos permite dividir o código em componentes separados que podem
mudar independentemente uns dos outros. Veremos então como isso leva naturalmente à parte OCP do SOLID.
Inversão de dependência (DI) significa que escrevemos código para depender de abstrações, não de detalhes. O
oposto disso é ter dois blocos de código, um que depende da implementação detalhada do outro. Alterações em
um bloco causarão alterações em outro. Para ver como é esse problema na prática, vamos revisar um contra-
exemplo. O trecho de código a seguir começa onde paramos com a classe Shapes após aplicar o SRP a ela:
formatos de embalagens;
importar java.util.ArrayList;
importar java.util.List;
caso "retângulo":
var r = (retângulo) s;
r.draw(g);
}
}
}
}
Este código funciona bem para manter uma lista de objetos Shape e desenhá-los. O problema é que ele sabe
muito sobre os tipos de formas que deve desenhar. O método draw() apresenta um
tipo de objeto de ativação que você pode ver. Isso significa que se alguma coisa mudar sobre quais tipos
de formas devem ser desenhadas, então este código também deverá mudar. Se quisermos adicionar um
novo Shape ao sistema, teremos que modificar esta instrução switch e o código de teste TDD associado.
O termo técnico para uma classe saber sobre outra é que existe uma dependência entre elas.
A classe Shapes depende das classes TextBox e Rectangle . Podemos representar isso visualmente
no seguinte diagrama de classes UML:
Machine Translated by Google
Podemos ver que a classe Shapes depende diretamente do detalhe do Rectangle e TextBox
Aulas. Isto é mostrado pela direção das setas no diagrama de classes UML. Ter essas dependências dificulta o
trabalho com a classe Shapes pelos seguintes motivos:
• Temos que alterar a classe Shapes para adicionar um novo tipo de forma
• Quaisquer alterações nas classes concretas, como Rectangle , farão com que este código mude
Esta é uma abordagem bastante processual para criar uma classe que lide com vários tipos de formas.
Ele viola o SRP ao fazer muito e saber muitos detalhes sobre cada tipo de objeto de forma. As formas
class depende dos detalhes de classes concretas como Rectangle e TextBox, o que causa diretamente os
problemas mencionados acima.
Felizmente, existe uma maneira melhor. Podemos usar o poder de uma interface para melhorar isso,
fazendo com que a classe Shapes não dependa desses detalhes. Isso é chamado de DI. Vamos ver
como fica a seguir.
formatos de embalagens;
Essa interface é nossa abstração da responsabilidade única que cada forma possui. Cada forma deve saber
desenhar a si mesma quando chamamos o método draw() . O próximo passo é fazer com que nossas classes
de formas concretas implementem essa interface.
Vamos pegar a classe Rectangle como exemplo. Você pode ver isso aqui:
@Sobrepor
Agora introduzimos o conceito OO de polimorfismo em nossas classes de forma. Isso quebra a dependência
que a classe Shapes tem de conhecer as classes Rectangle e TextBox .
Tudo o que a classe Shapes depende agora é da interface Shape . Não é mais necessário saber o
tipo de cada forma.
}
}
Essa refatoração removeu completamente a instrução switch e o método getType(), tornando o código
muito mais simples de entender e testar. Se adicionarmos um novo tipo de forma, a classe Shapes não
precisará mais ser alterada. Quebramos essa dependência de conhecer os detalhes das classes de forma.
Um pequeno refatorador move o parâmetro Graphics que passamos para o método draw() para um
campo, inicializado no construtor, conforme ilustrado no seguinte trecho de código:
Este é o DIP no trabalho. Criamos uma abstração na interface Shape. A classe Shapes é
consumidora dessa abstração. As classes que implementam essa interface são provedores. Ambos
os conjuntos de classes dependem apenas da abstração; eles não dependem de detalhes um do
outro. Não há referências à classe Rectangle na classe Shapes e não há referências às Shapes
dentro da classe Rectangle. Podemos ver essa inversão de dependências visualizada no seguinte
diagrama de classes UML – veja como a direção das setas de dependência mudou em comparação com a Figura 7.4:
Machine Translated by Google
Nesta versão do diagrama UML, as setas que descrevem as dependências entre classes apontam na direção oposta. As
dependências foram invertidas – daí o nome deste princípio. Nossas formas
class agora depende de nossa abstração, a interface Shape . O mesmo acontece com todas as implementações
concretas da classe Rectangle e da classe TextBox . Invertemos o gráfico de dependência e viramos as setas
de cabeça para baixo. DI desacopla totalmente as classes umas das outras e, como tal, é muito poderoso.
Veremos como isso leva a uma técnica chave para testes de TDD quando examinarmos o Capítulo 8, Test
Doubles – Stubs e Mocks.
MERGULHAR
Vimos como o DIP é uma ferramenta importante que podemos usar para simplificar nosso código. Ele nos permite escrever
código que lide com uma interface e, em seguida, usar esse código com qualquer classe concreta que implemente essa interface.
Isso levanta uma questão: podemos escrever uma classe que implemente uma interface, mas não funcione corretamente?
Esse é o assunto da nossa próxima seção.
Vimos na seção anterior sobre DIP como podemos usar qualquer classe que implemente uma interface no lugar da própria
interface. Também vimos como essas classes podem fornecer qualquer implementação que desejarem para esse método.
A interface em si não oferece nenhuma garantia sobre o que pode estar oculto nesse código de implementação.
Machine Translated by Google
É claro que há um lado ruim nisso – que o LSP pretende evitar. Vamos explicar isso observando um
contra-exemplo no código. Suponha que criamos uma nova classe que implementa a interface Shape,
como esta (Aviso: NÃO execute o código a seguir na classe MaliciousShape !):
Notou algo um pouco estranho nessa nova classe? Ele contém um comando Unix para remover todos os
nossos arquivos! Isso não é o que esperamos quando chamamos o método draw() em um objeto shape.
Devido a falhas de permissões, talvez não seja possível excluir nada, mas é um exemplo do que pode dar errado.
Uma interface em Java só pode proteger a sintaxe das chamadas de método que esperamos. Não pode
impor nenhuma semântica. O problema com a classe MaliciousShape anterior é que ela não respeita a
intenção por trás da interface.
O LSP nos orienta para evitar esse erro. Em outras palavras, o LSP afirma que qualquer classe que implemente
uma interface ou estenda outra classe deve lidar com todas as combinações de entrada que a classe/interface
original poderia. Deve fornecer os resultados esperados, não deve ignorar entradas válidas e não deve produzir
comportamentos completamente inesperados e indesejados. Classes escritas assim são seguras para uso por
meio de uma referência à sua interface. O problema com nossa classe MaliciousShape é que ela não era compatível
com LSP — ela adicionava algum comportamento extra totalmente inesperado e indesejado.
A cientista da computação americana Barbara Liskov apresentou uma definição formal: se p(x) é uma
propriedade demonstrável sobre objetos x do tipo T, então p(y) deveria ser verdadeiro para objetos y do tipo S
onde S é um subtipo de T.
Machine Translated by Google
@Sobrepor
O código anterior claramente pode manipular o desenho de qualquer texto válido fornecido ao seu construtor.
Também não oferece surpresas. Ele desenha o texto usando primitivas da classe Graphics e não faz mais nada.
Outros exemplos de conformidade com LSP podem ser vistos nas seguintes classes:
• Retângulo
• Triângulo
PSL
Um bloco de código pode ser trocado com segurança por outro se puder lidar com toda a gama de entradas e
fornecer (pelo menos) todas as saídas esperadas, sem efeitos colaterais indesejados.
Existem algumas violações surpreendentes do LSP. Talvez o clássico para o exemplo de código de formas seja
adicionar uma classe Square . Em matemática, um quadrado é uma espécie de retângulo, com a restrição extra
de que sua altura e largura sejam iguais. No código Java, devemos fazer a classe Square estender o Rectangle
aula? Que tal a classe Rectangle estendendo Square?
Vamos aplicar o LSP para decidir. Iremos imaginar um código que espera uma classe Rectangle para que possa alterar
sua altura, mas não sua largura. Se passássemos uma classe Square para esse código, ele funcionaria corretamente?
A resposta é não. Você teria então um quadrado com largura e altura desiguais. Isso falha no LSP.
O objetivo do LSP é fazer com que as classes estejam em conformidade com as interfaces. Na próxima seção,
veremos o OCP, que está intimamente relacionado ao DI.
Machine Translated by Google
Nesta seção, veremos como o OCP nos ajuda a escrever código ao qual podemos adicionar novos
recursos, sem alterar o código em si. A princípio, isso parece impossível, mas flui naturalmente do DIP
combinado com o LSP.
OCP resulta em código aberto à extensão, mas fechado à modificação. Vimos essa ideia em ação
quando analisamos o DIP. Vamos revisar a refatoração de código que fizemos à luz do OCP.
g.drawLine(0, r.getWidth());
}
}
}
}
}
Machine Translated by Google
Adicionar um novo tipo de forma requer modificação do código dentro do método draw() . Adicionaremos
uma nova declaração de caso para apoiar nosso novo formato.
• Poderemos introduzir um erro que quebre parte do suporte existente para formas.
• Podemos ter vários desenvolvedores adicionando formas ao mesmo tempo e obter um conflito de mesclagem quando
nós combinamos o trabalho deles.
Agora podemos ver que adicionar um novo tipo de forma não precisa de modificação neste código. Este é um
exemplo de OCP em ação. A classe Shapes está aberta à definição de novos tipos de formas, mas está fechada
à necessidade de modificação quando essa nova forma é adicionada. Isso também significa que quaisquer
testes relacionados à classe Shapes permanecerão inalterados, pois não há diferença de comportamento para
esta classe. Essa é uma vantagem poderosa.
OCP depende do DI para funcionar. É mais ou menos uma reformulação de uma consequência da aplicação do DIP.
Ele também nos fornece uma técnica para suportar comportamento trocável. Podemos usar DIP e OCP para criar
sistemas de plugins.
Machine Translated by Google
Para ver como isso funciona na prática, vamos criar um novo tipo de forma, a classe RightArrow , como segue:
A classe RightArrow implementa a interface Shape e define um método draw() . Para demonstrar
que nada na classe Shapes precisa mudar para poder usar isso, vamos revisar alguns códigos
que usam tanto Shapes quanto nossa nova classe, RightArrow, como segue:
formatos de embalagens;
formas.add(new TextBox("Olá!"));
formas.add(novo retângulo(32,1));
formas.add(new RightArrow());
formas.draw();
}
}
Vemos que a classe Shapes está sendo utilizada de forma completamente normal, sem alterações. Na verdade,
a única mudança necessária para usar nossa nova classe RightArrow é criar uma instância de objeto e passá-
la para o método add() de formas.
Machine Translated by Google
OCP
Torne o código aberto para novos comportamentos, mas fechado para modificações.
O poder do OCP agora deve estar claro. Podemos estender os recursos do nosso código e manter as alterações
limitadas. Reduzimos bastante o risco de quebra de código que já está funcionando, pois não precisamos mais
alterá-lo. OCP é uma ótima maneira de gerenciar a complexidade. Na próxima seção, veremos o princípio
SOLID restante: ISP.
O ISP nos aconselha a manter nossas interfaces pequenas e dedicadas a cumprir uma única responsabilidade. Por interfaces
pequenas, queremos dizer ter o menor número possível de métodos em uma única interface. Todos esses métodos devem
estar relacionados a algum tema comum.
Podemos ver que este princípio é na verdade apenas SRP em outra forma. Estamos dizendo que uma interface
eficaz deve descrever uma única responsabilidade. Deve abranger uma abstração, não várias. Os métodos na
interface devem estar fortemente relacionados entre si e também com aquela abstração única.
Se precisarmos de mais abstrações, usaremos mais interfaces. Mantemos cada abstração em sua própria
interface separada, de onde vem o termo segregação de interface - mantemos diferentes abstrações separadas.
O cheiro de código relacionado a isso é uma interface grande que cobre vários tópicos diferentes em um.
Poderíamos imaginar uma interface com centenas de métodos em pequenos grupos – alguns relacionados ao
gerenciamento de arquivos, alguns à edição de documentos e alguns à impressão de documentos. Essas interfaces
rapidamente se tornam difíceis de trabalhar. O ISP sugere que melhoremos isso dividindo a interface em várias
interfaces menores. Essa divisão preservaria os grupos de métodos — portanto, você poderia ver interfaces para
gerenciamento, edição e impressão de arquivos, com métodos relevantes em cada uma. Tornamos nosso código
mais simples de entender , separando essas abstrações separadas.
interface Forma {
desenho vazio (Gráficos g);
}
Esta interface claramente tem um único foco. É uma interface com um foco muito restrito, tanto que
apenas um método precisa ser especificado: draw(). Não há confusão decorrente de outras misturas
Machine Translated by Google
conceitos aqui e sem métodos desnecessários. Esse único método é necessário e suficiente. O outro
exemplo importante está na interface gráfica , conforme mostrado aqui:
A interface gráfica contém apenas métodos relacionados ao desenho de primitivos gráficos na tela.
Ele possui dois métodos: drawText para exibir uma string de texto e drawHorizontalLine para desenhar uma linha
na direção horizontal. Como esses métodos estão fortemente relacionados – conhecidos tecnicamente por
apresentarem alta coesão – e são poucos em número, o ISP está satisfeito. Esta é uma abstração eficaz sobre o
subsistema de desenho gráfico, adaptada aos nossos propósitos.
Para completar, podemos implementar essa interface de várias maneiras. O exemplo no GitHub usa uma
implementação de console de texto simples:
@Sobrepor
imprimir(rowText.toString());
}
Resumo 117
Essa implementação também é compatível com LSP – ela pode ser usada onde quer que a interface gráfica
seja esperada.
ISP
Agora cobrimos todos os cinco princípios do SOLID e mostramos como eles foram aplicados ao código de formas. Eles
orientaram o design em direção a um código compacto, com uma estrutura bem projetada
para auxiliar futuros mantenedores. Sabemos como incorporar esses princípios em nosso próprio código para obter
benefícios semelhantes.
Resumo
Neste capítulo, vimos explicações simples de como os princípios SOLID nos ajudam a projetar nosso código de produção
e nossos testes. Trabalhamos em um projeto de exemplo que usa todos os cinco princípios SOLID. Em trabalhos futuros,
podemos aplicar o SRP para nos ajudar a compreender o nosso design e limitar o retrabalho envolvido em mudanças
futuras. Podemos aplicar DIP para dividir nosso código em pequenos pedaços independentes, deixando cada pedaço
ocultando alguns detalhes de nosso programa geral, criando um efeito de dividir e conquistar. Usando LSP, podemos
criar objetos que podem ser trocados com segurança e facilidade. OCP nos ajuda a projetar software simples de adicionar
funcionalidade. O ISP manterá nossas interfaces pequenas e fáceis de entender.
O próximo capítulo coloca esses princípios em uso para resolver um problema de teste – como testamos as colaborações
entre nossos objetos?
Perguntas e respostas
1. Os princípios SOLID se aplicam apenas ao código OO?
Não. Embora originalmente aplicados a um contexto OO, eles têm uso tanto na programação funcional quanto
no design de microsserviços. O SRP é quase universalmente útil – manter um foco principal é útil para qualquer
coisa, até mesmo para parágrafos de documentação. O pensamento SRP também nos ajuda a escrever uma
função pura que faz apenas uma coisa e um teste que faz apenas uma coisa. DIP e OCP são facilmente
executados em contextos funcionais, passando a dependência como uma função pura, como fazemos com
lambdas Java. O SOLID como um todo fornece um conjunto de metas para gerenciar o acoplamento e a coesão
entre qualquer tipo de componente de software.
Não. O TDD funciona definindo os resultados e a interface pública de um componente de software. A forma como
implementamos esse componente é irrelevante para um teste TDD, mas o uso de princípios como SRP e DIP
torna muito mais fácil escrever testes nesse código, fornecendo-nos os pontos de acesso de teste de que precisamos.
Machine Translated by Google
Os princípios SOLID são um excelente ponto de partida para moldar seu código e devemos aproveitá
-los, mas existem muitas outras técnicas válidas para projetar software. Todo o catálogo de padrões de
projeto, o excelente sistema de Padrões de Software de Atribuição de Responsabilidade Geral
(GRASP) de Craig Larman, a ideia de ocultação de informações de David L. Parnas e as ideias de
acoplamento e coesão, todas se aplicam. Devemos usar toda e qualquer técnica que conhecemos — ou
sobre a qual podemos aprender — para servir ao nosso objetivo de criar software que seja fácil de ler e seguro para alteração
Sim muito mesmo. O TDD se preocupa em testar o comportamento do código, não nos detalhes de como ele é
implementado. Os princípios SOLID simplesmente nos ajudam a criar projetos OO que são robustos e mais simples
de testar.
O ISP nos orienta a preferir muitas interfaces mais curtas em vez de uma interface grande. Cada uma das interfaces
mais curtas deve estar relacionada a um único aspecto do que uma classe deve fornecer. Geralmente é algum tipo
de função ou talvez um subsistema. Pode-se considerar que o ISP garante que cada uma de nossas interfaces
aplique o SRP e faça apenas uma coisa: bem.
OCP nos orienta na criação de componentes de software que podem ter novos recursos adicionados sem alterar o
componente em si. Isso é feito usando um design de plugin. O componente permitirá que classes separadas sejam
conectadas, fornecendo os novos recursos. A maneira de fazer isso é criar uma abstração do que um plugin deve
fazer em uma interface – DIP. Em seguida, crie implementações concretas de plug-ins em conformidade com o
LSP. Depois disso, podemos injetar esses novos plugins em nosso componente. OCP depende de DIP e LSP para
funcionar.
Machine Translated by Google
8
Duplas de teste – Stubs e simulações
Neste capítulo, resolveremos um desafio comum de teste. Como você testa um objeto que
depende de outro objeto? O que faremos se for difícil configurar esse colaborador com dados de teste?
Várias técnicas estão disponíveis para nos ajudar com isso e baseiam-se nos princípios SOLID que aprendemos
anteriormente. Podemos usar a ideia de injeção de dependência para nos permitir substituir objetos colaboradores por
outros especialmente escritos para nos ajudar a escrever nosso teste.
Esses novos objetos são chamados de testes duplos, e aprenderemos sobre dois tipos importantes de testes
duplos neste capítulo. Aprenderemos quando aplicar cada tipo de teste duplo e, em seguida, aprenderemos
duas maneiras de criá-los em Java – escrevendo nós mesmos o código e usando a popular biblioteca Mockito.
Ao final do capítulo, teremos técnicas que nos permitem escrever testes para objetos onde é difícil ou impossível testá-los
com os objetos colaboradores reais instalados. Isso nos permite usar TDD com sistemas complexos.
Requerimentos técnicos
O código deste capítulo pode ser encontrado em https://github.com/PacktPublishing/
Test Driven-Development-with-Java/tree/main/chapter08.
Machine Translated by Google
À medida que desenvolvemos nosso sistema de software, em breve superaremos o que pode existir em uma única
classe (ou função, nesse caso). Dividiremos nosso código em várias partes. Se escolhermos um único objeto como
nosso sujeito em teste, qualquer outro objeto do qual ele dependa será um colaborador. Nossos testes TDD devem
levar em conta a presença desses colaboradores. Às vezes, isso é simples, como vimos nos capítulos anteriores.
Infelizmente, as coisas nem sempre são tão simples. Algumas colaborações tornam os testes difíceis – ou
impossíveis – de escrever. Esses tipos de colaboradores introduzem comportamentos irrepetíveis que
devemos enfrentar ou apresentam erros difíceis de desencadear.
Vamos revisar esses desafios com alguns exemplos curtos. Começaremos com um problema comum: um
colaborador que apresenta um comportamento irrepetível.
Para ilustrar, vamos revisar uma classe que lança um dado e apresenta uma sequência de texto para dizer o que lançamos:
exemplos de pacotes;
importar java.util.random.RandomGenerator;
Este é um código bastante simples, com apenas algumas linhas executáveis. Infelizmente, simples de escrever nem
sempre é simples de testar. Como escreveríamos um teste para isso? Especificamente – como escreveríamos a afirmação?
Nos testes anteriores, sempre soubemos exatamente o que esperar da afirmação. Aqui, a afirmação será
algum texto fixo mais um número aleatório. Não sabemos antecipadamente qual será esse número aleatório.
Para ilustrar, vamos imaginar um código para nos avisar quando a bateria do nosso dispositivo portátil está fraca:
O código anterior no BatteryMonitor apresenta uma classe DeviceApi , que é uma classe de biblioteca que
nos permite ler quanta bateria resta em nosso telefone. Ele fornece um método estático para fazer isso,
chamado getBatteryPercentage(). Isso retornará um número inteiro no intervalo de 0 a 100 por cento. O
código para o qual queremos escrever um teste TDD chama getBatteryPercentage() e exibirá uma
mensagem de aviso se for inferior a 10 por cento. Mas há um problema ao escrever este teste: como
podemos forçar o método getBatteryPercentage() a retornar um número menor que 10 como parte de
nossa etapa de organização? Descarregaríamos a bateria de alguma forma? Como faríamos isso?
BatteryMonitor fornece um exemplo de código que colabora com outro objeto, onde é impossível
forçar uma resposta conhecida desse colaborador. Não temos como alterar o valor que
getBatteryPercentage() retornará. Teríamos literalmente que esperar até que a bateria
descarregasse antes que o teste pudesse passar. Não é disso que se trata o TDD.
A melhor forma de escrever testes nesses casos é eliminando a causa da dificuldade. Felizmente, existe
uma solução simples. Podemos aplicar o Princípio de Injeção de Dependência que aprendemos no capítulo
anterior, juntamente com uma nova ideia – o teste duplo. Analisaremos as duplas de teste na próxima seção.
Machine Translated by Google
Os desafios da seção anterior são resolvidos usando testes duplos. Um teste duplo substitui um dos
objetos colaboradores em nosso teste. Por design, este teste duplo evita as dificuldades do objeto
substituído. Pense neles como dublês nos filmes, substituindo os atores reais para ajudar a conseguir uma
cena de ação com segurança.
Um teste duplo de software é um objeto que escrevemos especificamente para ser fácil de usar em nosso teste
de unidade. No teste, injetamos nosso teste duplo no SUT na etapa de organização. No código de produção,
injetamos o objeto de produção que nosso duplo de teste substituiu.
Vamos reconsiderar nosso exemplo DiceRoll anterior. Como refatoraríamos esse código para torná-lo mais fácil
testar?
2. Aplique o Princípio de Inversão de Dependência à classe DiceRoll para fazer uso desta abstração:
exemplos de pacotes;
importar java.util.random.RandomGenerator;
enrolado);
}
}
exemplos de pacotes;
classe DiceRollTest {
@Teste
void produzMensagem() {
var stub = new StubRandomNumbers();
var roll = new DiceRoll(stub);
assertThat(actual).isEqualTo("Você rolou um
5");
}
}
Vemos as seções habituais Arrange, Act e Assert neste teste. A nova ideia aqui é a classe
StubRandomNumbers. Vejamos o código stub:
exemplos de pacotes;
@Sobrepor
Há algumas coisas a serem observadas sobre este esboço. Em primeiro lugar, implementa nossos RandomNumbers
interface, tornando-o um substituto compatível com LSP para essa interface. Isso nos permite
injetá-lo no construtor do DiceRoll, nosso SUT. O segundo aspecto mais importante é que cada
chamada para nextInt() retornará o mesmo número.
Ao substituir a fonte real de RandomNumbers por um stub que fornece um valor conhecido,
tornamos nossa afirmação de teste fácil de escrever. O stub elimina o problema de valores
irrepetíveis do gerador aleatório.
Agora podemos ver como funciona o DiceRollTest . Fornecemos um teste duplo para nosso SUT. O teste duplo
sempre retorna o mesmo valor. Como resultado, podemos afirmar contra um resultado conhecido.
@Sobrepor
retornar rnd.nextInt(upperBoundExclusive);
}
}
Não há muito trabalho a fazer aqui – o código anterior simplesmente implementa o método nextInt() usando
a classe da biblioteca RandomGenerator incorporada em Java.
Agora podemos usar isso para criar nossa versão de produção do código. Já alteramos nosso DiceRoll
classe para nos permitir injetar qualquer implementação adequada da interface RandomNumbers . Para
nosso código de teste, injetamos um teste duplo – uma instância da classe StubRandomNumbers . Para
nosso código de produção, em vez disso, injetaremos uma instância do RandomlyGeneratedNumbers
aula. O código de produção usará esse objeto para criar números aleatórios reais – e não haverá alterações
de código dentro da classe DiceRoll . Usamos o Princípio de Inversão de Dependência para tornar a classe
DiceRoll configurável por injeção de dependência. Isso significa que a classe DiceRoll agora segue o Princípio
Aberto/Fechado – ela está aberta a novos tipos de comportamento de geração de números aleatórios, mas
fechada a alterações de código dentro da própria classe.
Machine Translated by Google
System.out.println(roll.asText());
}
}
Você pode ver no código anterior que injetamos uma instância da classe
RandomlyGeneratedNumbers da versão de produção na classe DiceRoll . Este processo de
criação e injeção de objetos é frequentemente denominado fiação de objetos. Estruturas como Spring (https://sp
io/), Google Guice (https://github.com/google/guice) e o Java CDI integrado (https://
docs.oracle.com/javaee/6/tutorial/doc/giwhl.html) fornecem maneiras de minimizar o
padrão de criação de dependências e conectá-las, usando anotações.
Usar DIP para trocar um objeto de produção por um teste duplo é uma técnica muito poderosa. Este teste
duplo é um exemplo de um tipo de duplo conhecido como stub. Abordaremos o que é um stub e quando
usá-lo na próxima seção.
Machine Translated by Google
No exemplo anterior do DiceRoll , o teste foi mais simples de escrever porque substituímos a geração de
números aleatórios por um valor fixo conhecido. Nosso gerador genuíno de números aleatórios dificultou a
escrita de uma afirmação, pois nunca tínhamos certeza de qual deveria ser o número aleatório esperado.
Nosso duplo de teste era um objeto que fornecia um valor bem conhecido. Podemos então calcular o valor
esperado para nossa afirmação, tornando nosso teste fácil de escrever.
Um teste duplo que fornece valores como esse é chamado de stub. Os stubs sempre substituem um objeto que não podemos
controlar por uma versão somente de teste que podemos controlar. Eles sempre produzem valores de dados conhecidos para
serem consumidos pelo nosso código em teste. Graficamente, um esboço se parece com isto:
No diagrama, nossa classe de teste é responsável por conectar nosso SUT a um objeto stub apropriado na etapa de
organização. Quando a etapa Act pede ao nosso SUT para executar o código que queremos testar, esse código
extrairá os valores de dados conhecidos do stub. A etapa Assert pode ser escrita com base no comportamento
esperado que esses valores de dados conhecidos causarão.
É importante observar por que isso funciona. Uma objecção a este acordo é que não estamos a testar o
sistema real. Nosso SUT está conectado a algum objeto que nunca fará parte do nosso sistema de produção.
Isso é verdade. Mas isso funciona porque nosso teste testa apenas a lógica dentro do SUT. Este teste não é
testando o comportamento das próprias dependências. Na verdade, não deve tentar fazer isso. Testar o teste duplo é
um antipadrão clássico para testes unitários.
Nosso SUT usou o Princípio de Inversão de Dependência para se isolar totalmente do objeto que o stub representa.
Não faz diferença para o SUT como ele obtém os dados de seu colaborador. É por isso que esta abordagem de teste
é válida.
Machine Translated by Google
• Stubbing de uma interface/banco de dados de repositório: usando um stub em vez de chamar um banco de dados real para obter
código de acesso a dados
• Stubbing de fontes de dados de referência: substituição de arquivos de propriedades ou serviços da Web que contêm referência
dados com dados stub
• Fornecer objetos de aplicativo para código que converte em formatos HTML ou JSON: ao testar código que converte em HTML
• Stubbing do relógio do sistema para testar o comportamento dependente do tempo: para obter um comportamento repetível de
uma chamada de tempo, stub a chamada com horários conhecidos
• Stubbing de geradores de números aleatórios para criar previsibilidade: Substitua uma chamada para um gerador de números
• Stubbing de sistemas de autenticação para sempre permitir que um usuário de teste faça login: Substitua chamadas para
• Stubbing de uma chamada para um comando do sistema operacional: Substitua uma chamada para o sistema operacional para,
Nesta seção, vimos como o uso de stubs nos permite controlar os dados que são fornecidos a um SUT.
Ele suporta um modelo pull de busca de objetos de outro lugar. Mas esse não é o único mecanismo pelo qual os objetos podem colaborar.
Alguns objetos usam um modelo push. Neste caso, quando chamamos um método no nosso SUT, esperamos que ele chame outro método
em algum outro objeto. Nosso teste deve confirmar se esta chamada de método realmente ocorreu. Isso é algo que os stubs não podem
ajudar e precisa de uma abordagem diferente. Abordaremos essa abordagem na próxima seção.
Objetos simulados são uma espécie de teste duplo que registra interações. Ao contrário dos stubs, que fornecem objetos bem conhecidos
ao SUT, uma simulação simplesmente registrará as interações que o SUT tem com a simulação. É a ferramenta perfeita para responder à
pergunta: “O SUT chamou o método corretamente?” Isso resolve o problema das interações do modelo push entre o SUT e seu colaborador.
O SUT ordena ao colaborador que faça algo em vez de solicitar algo dele. Uma simulação fornece uma maneira de verificar se esse comando
Vemos nosso código de teste conectando um objeto simulado ao SUT. A etapa Act fará com que o SUT
execute o código que esperamos interagir com seu colaborador. Trocamos esse colaborador por um mock,
que registrará o fato de que um determinado método foi chamado nele.
Vejamos um exemplo concreto para facilitar a compreensão. Suponha que nosso SUT envie um e-mail
para um usuário. Mais uma vez, usaremos o Princípio da Inversão de Dependência para criar uma
abstração do nosso servidor de e-mail como interface:
O código anterior mostra uma interface simplificada adequada apenas para enviar um email de texto curto.
É bom o suficiente para nossos propósitos. Para testar o SUT que chamou o método sendEmail() nesta
interface, escreveríamos uma classe MockMailServer :
@Sobrepor
}
}
A classe MockMailServer anterior implementa a interface MailServer . Ele tem uma única responsabilidade
– registrar o fato de que o método sendEmail() foi chamado e capturar os valores reais dos parâmetros
enviados para esse método. Ele os expõe como campos simples com visibilidade pública do pacote. Nosso
código de teste pode usar esses campos para formar a afirmação. Nosso teste simplesmente precisa
conectar esse objeto simulado ao SUT, fazer com que o SUT execute o código que esperamos chamar de sendEmail()
método e, em seguida, verifique se ele fez isso:
@Teste
notificações.welcomeNewUser();
assertThat(mailServer.wasCalled).isTrue();
assertThat(mailServer.actualRecipient)
.isEqualTo("teste@exemplo.com");
assertThat(mailServer.actualSubject)
.isEqualTo("Bem-vindo!");
assertThat(mailServer.actualText)
.contains("Bem-vindo à sua conta");
}
Podemos ver que este teste conecta a simulação ao nosso SUT e, em seguida, faz com que o SUT execute o
método WelcomeNewUser() . Esperamos que este método chame o método sendEmail() no objeto MailServer .
Então, precisamos escrever asserções para confirmar que a chamada foi feita com os valores corretos dos
parâmetros passados. Estamos usando a ideia de quatro declarações de afirmação logicamente aqui e testando
uma ideia – efetivamente uma única afirmação.
O poder dos objetos simulados é que podemos registrar interações com objetos que são difíceis de controlar.
No caso de um servidor de e-mail, como o visto no bloco de código anterior, não gostaríamos de enviar e-
mails reais para ninguém. Também não gostaríamos de escrever um teste que esperasse monitorar a
caixa de correio de um usuário de teste. Isso não só é lento e pode não ser confiável, mas também não é
o que pretendemos testar. O SUT tem apenas a responsabilidade de fazer a chamada para sendEmail() –
o que acontece depois disso está fora do escopo do SUT. Está, portanto, fora do escopo deste teste.
Machine Translated by Google
Como nos exemplos anteriores com outros testes duplos, o fato de termos usado o Princípio de Inversão de
Dependência significa que nosso código de produção é bastante fácil de criar. Precisamos simplesmente criar
uma implementação do MailServer que use o protocolo SMTP para se comunicar com um servidor de correio
real. Provavelmente procuraríamos uma classe de biblioteca que já fizesse isso para nós, então precisaríamos
criar um objeto adaptador muito simples que vinculasse o código da biblioteca à nossa interface.
Esta seção abordou dois tipos comuns de testes duplos, stubs e simulações. Mas os testes duplos nem sempre são
apropriados para uso. Na próxima seção, discutiremos algumas questões que você deve conhecer ao usar testes duplos.
À primeira vista, o uso de objetos simulados parece resolver vários problemas para nós. No entanto, se usados sem
cuidado, podemos acabar com testes de qualidade muito baixa. Para entender o porquê, vamos voltar à nossa definição
básica de teste TDD. É um teste que verifica comportamentos e é independente de implementações. Se usarmos um objeto
simulado para substituir uma abstração genuína, então estaremos cumprindo isso.
O problema potencial acontece porque é muito fácil criar um objeto simulado para um detalhe de implementação, não para
uma abstração. Se fizermos isso, acabaremos prendendo nosso código em uma implementação e estrutura específicas.
Depois que um teste é acoplado a um detalhe de implementação específico, a alteração dessa implementação requer uma
alteração no teste. Se a nova implementação tiver os mesmos resultados que a antiga, o teste ainda deverá passar. Testes
que dependem de detalhes específicos de implementação ou estruturas de código impedem ativamente a refatoração e a
adição de novos recursos.
Outra área onde os mocks não devem ser usados é como substitutos para uma classe concreta escrita fora
da sua equipe. Suponha que estejamos usando uma classe chamada PdfGenerator de uma biblioteca para criar um
documento PDF. Nosso código chamaria métodos na classe PdfGenerator . Poderíamos pensar que seria fácil testar nosso
código se usássemos um objeto simulado para substituir a classe PdfGenerator .
Esta abordagem tem um problema que só surgirá no futuro. A classe na biblioteca externa provavelmente
mudará. Digamos que a classe PdfGenerator remova um dos métodos que nosso código está chamando.
Seremos forçados a atualizar a versão da biblioteca em algum momento, como parte de nossa política de
segurança, pelo menos. Quando recebermos a nova versão, nosso código não será mais compilado com base nesta alteração
Machine Translated by Google
class – mas nossos testes ainda serão aprovados porque o objeto simulado ainda contém o método antigo. Esta é
uma armadilha sutil que preparamos para futuros mantenedores do código. É melhor evitar. Uma abordagem razoável
é agrupar a biblioteca de terceiros e, idealmente, colocá-la atrás de uma interface para inverter a dependência dela ,
isolando-a totalmente.
A pista de que algo é um objeto de valor em Java é a presença de um método equals() e hashCode()
personalizados. Por padrão, Java compara a igualdade de dois objetos usando sua identidade – ele
verifica se duas referências de objeto estão se referindo à mesma instância de objeto na memória.
Devemos substituir os métodos equals() e hashCode() para fornecer o comportamento correto para
objetos de valor, com base em seu conteúdo.
Um objeto de valor é uma coisa simples. Pode ter alguns comportamentos complexos dentro de seus métodos,
mas, em princípio, objetos de valor devem ser fáceis de criar. Não há benefício em criar um objeto simulado
para substituir um desses objetos de valor. Em vez disso, crie o objeto de valor e use-o em seu teste.
Os duplos de teste só podem ser usados onde podemos injetá-los. Isto nem sempre é possível. Se o código que
queremos testar cria uma classe concreta usando a palavra-chave new , então não podemos substituí-la por um double:
exemplos de pacotes;
Vemos que o campo profiles foi inicializado usando uma classe concreta UserProfilesPostgres().
Não existe uma maneira direta de injetar um teste duplo com este design. Poderíamos tentar contornar isso
usando Java Reflection, mas é melhor considerar isso como um feedback do TDD sobre uma limitação do
nosso design. A solução é permitir que a dependência seja injetada, como vimos nos exemplos anteriores.
Isso geralmente é um problema com código legado, que é simplesmente um código que foi escrito antes de
trabalharmos nele. Se este código criou objetos concretos – e o código não pode ser alterado – então não
podemos aplicar um teste duplo.
Testar a simulação é uma frase usada para descrever um teste com muitas suposições incorporadas em um teste duplo.
Suponha que escrevemos um stub que representa algum acesso ao banco de dados, mas esse stub contém centenas
de linhas de código para emular consultas específicas detalhadas a esse banco de dados. Quando escrevermos as
asserções de teste, todas elas serão baseadas nas consultas detalhadas que emulamos no stub.
Essa abordagem provará que a lógica SUT responde a essas consultas. Mas nosso esboço agora pressupõe muito
sobre como o código real de acesso a dados funcionará. O código stub e o código real de acesso a dados podem
rapidamente ficar fora de sintonia. Isso resulta em um teste de unidade inválido que passa, mas com respostas
fragmentadas que não podem mais acontecer na realidade.
Mocks são úteis sempre que nosso SUT está usando um modelo push e solicitando uma ação de algum outro
componente, onde não há resposta óbvia, como a seguinte:
• Solicitar uma ação de um serviço remoto, como enviar um email para um servidor de email
• Invalidando um cache
Aprendemos algumas técnicas nesta seção que nos permitem verificar se uma ação foi solicitada.
Vimos como podemos usar o Princípio da Inversão de Dependência mais uma vez para nos permitir injetar um teste
duplo que podemos consultar. Também vimos um exemplo de código escrito à mão para fazer isso. Mas devemos
sempre escrever os testes duplos à mão? Na próxima seção, abordaremos uma biblioteca muito útil que faz a maior
parte do trabalho para nós.
Machine Translated by Google
Mockito é uma biblioteca de código aberto gratuita sob licença do MIT. Esta licença significa que podemos usá -la para trabalhos de
desenvolvimento comercial, sujeito ao acordo daqueles para quem trabalhamos. Mockito oferece uma ampla gama de recursos
destinados a criar testes duplos com muito pouco código. O site do Mockito pode ser encontrado em https://site.mockito.org/.
Começar com o Mockito é simples. Extraímos a biblioteca Mockito e uma biblioteca de extensão em
nosso arquivo Gradle. A biblioteca de extensão permite que o Mockito se integre intimamente ao JUnit5.
dependências {
testImplementation 'org.junit.jupiter:junit-jupiter api:5.8.2'
testImplementation 'org.assertj:assertj-core:3.22.0'
testImplementation 'org.mockito:mockito-core:4.8.0'
testImplementation 'org.mockito:mockito-junit jupiter:4.8.0'
Vamos ver como o Mockito nos ajuda a criar um objeto stub. Usaremos TDD para criar uma classe
UserGreeting que entrega uma saudação personalizada, após buscar nosso apelido na interface UserProfiles.
Vamos escrever isso usando pequenos passos, para ver como o TDD e o Mockito funcionam juntos:
exemplos de pacotes
importar org.junit.jupiter.api.extension.ExtendWith;
importar org.mockito.junit.jupiter.MockitoExtension;
Machine Translated by Google
UserGreetingTest { }
exemplos de pacotes;
importar org.junit.jupiter.api.Test;
importar org.junit.jupiter.api.extension.ExtendWith;
importar org.mockito.junit.jupiter.MockitoExtension; importar estático
org.assertj.core.api.Assertions.assertThat;
UserGreetingTest {
@Teste
void formatosGreetingWithName() {
Este é o uso padrão das estruturas JUnit e AssertJ, como vimos antes. Se executarmos o teste agora, ele
falhará.
3. Exclua nosso SUT – a classe que queremos escrever – com uma etapa Act:
exemplos de pacotes;
@ExtendWith(MockitoExtension.class)
@Teste
void formatosGreetingWithName() {
String real =
saudação.formatGreeting(USER_ID);
afirmarIsso(real)
}
}
Esta etapa elimina as duas novas classes de código de produção, conforme mostrado nas etapas a seguir.
exemplos de pacotes;
Como de costume, não adicionamos nenhum código além do necessário para compilar nosso teste. A decisão
de design capturada aqui mostra que nosso comportamento é fornecido por um método formatGreeting(), que
identifica um usuário por uma classe UserId .
Machine Translated by Google
exemplos de pacotes;
Novamente, obtemos um shell vazio apenas para compilar o teste. Então, executamos o teste e ele ainda falha:
6. Outra decisão de design a ser capturada é que a classe UserGreeting dependerá de uma interface
UserProfiles. Precisamos criar um campo, criar o esqueleto da interface e injetar o campo em um
novo construtor para o SUT:
exemplos de pacotes;
@Teste
void formatosGreetingWithName() {
var saudação
= novo UserGreeting(perfis);
String real =
saudação.formatoGreeting(USER_ID);
afirmarIsso(real)
.isEqualTo("Olá e seja bem-vindo, Alan");
}
}
exemplos de pacotes;
exemplos de pacotes;
O teste falha porque passamos o campo profiles como uma dependência em nosso SUT, mas
esse campo nunca foi inicializado. É aqui que Mockito entra em jogo (finalmente).
exemplos de pacotes;
@Zombar
@Teste
void formatosGreetingWithName() {
String real =
saudação.formatGreeting(USER_ID);
afirmarIsso(real)
.isEqualTo("Olá e seja bem-vindo, Alan");
}
}
A execução do teste agora produz uma falha diferente, pois ainda não configuramos o mock do Mockito:
11. Configure @Mock para retornar os dados de stub corretos para nosso teste:
exemplos de pacotes;
importar org.junit.jupiter.api.Test;
importar org.junit.jupiter.api.extension.ExtendWith;
importar org.mockito.Mock;
Machine Translated by Google
UserGreetingTest {
@Zombar
@Teste
void formatosGreetingWithName()
{ quando(profiles.fetchNicknameFor(USER_ID))
.thenReturn("Alan");
String real =
saudação.formatGreeting(USER_ID);
afirmarIsso(real)
.isEqualTo("Olá e seja bem-vindo, Alan");
}
}
12. Se você executar o teste novamente, ele falhará devido a um erro no texto da saudação. Corrija isso e então
execute novamente o teste e ele passará:
Machine Translated by Google
Acabamos de criar a classe UserGreeting, que acessa alguns apelidos armazenados do usuário, através da interface
UserProfiles. Essa interface usou DIP para isolar UserGreeting de quaisquer detalhes de implementação dessa loja.
Usamos uma implementação stub para escrever o teste. Seguimos o TDD e aproveitamos o Mockito para escrever esse
esboço para nós.
Você também notará que o teste falhou na etapa final. Eu esperava que essa etapa fosse aprovada. Não aconteceu
porque eu digitei a mensagem de saudação incorretamente. Mais uma vez, o TDD veio em meu socorro.
Mockito pode criar objetos simulados tão facilmente quanto stubs. Ainda podemos usar a anotação @Mock em
um campo que desejamos tornar uma simulação – talvez finalmente dando sentido à anotação. Usamos o
método Mockito verify() para verificar se nosso SUT chamou um método esperado em um colaborador.
Vejamos como uma simulação é usada. Escreveremos um teste para algum código SUT que esperamos enviar por e-mail
via servidor de correio:
@ExtendWith(MockitoExtension.class)
classe WelcomeEmailTest {
@Zombar
@Teste
= novo UserNotifications(mailServer);
notificações.welcomeNewUser("test@example.com");
verificar(mailServer).sendEmail("teste@example.com",
"Bem-vindo!",
Machine Translated by Google
}
}
Mockito usa geração de código para conseguir tudo isso. Ele envolve a interface que rotulamos com @Mock
anotação e intercepta cada chamada. Ele armazena valores de parâmetros para cada chamada. Quando
usamos o método verify() para confirmar se o método foi chamado corretamente, Mockito tem todos os dados
necessários para fazer isso.
* quando(object.method()).thenReturn(valor esperado);
* verificar(objeto).método();
Configurar um teste duplo para ser um esboço e uma simulação é um cheiro de código de teste. Não está errado,
mas vale a pena fazer uma pausa para pensar. Devemos considerar se o colaborador que estamos zombando e
criticando confundiu algumas responsabilidades. Pode ser benéfico dividir esse objeto.
Até agora, configuramos os testes duplos do Mockito para responder a entradas muito específicas dos métodos que
eles substituem. O exemplo anterior do MailServer verificou três valores de parâmetros específicos sendo passados
para a chamada do método sendEmail() . Mas às vezes queremos mais flexibilidade em nossos testes duplos.
Mockito fornece métodos de biblioteca chamados correspondentes de argumentos. Esses são métodos estáticos
usados dentro das instruções when() e verify() . Correspondentes de argumentos são usados para instruir o Mockito
a responder a um intervalo de valores de parâmetros – incluindo nulos e valores desconhecidos – que podem ser
passados para um método em teste.
Machine Translated by Google
O teste a seguir usa uma correspondência de argumentos que aceita qualquer valor de UserId:
exemplos de pacotes2;
importar exemplos.UserGreeting;
importar exemplos.UserId;
importar exemplos.UserProfiles; importar
org.junit.jupiter.api.Test; importar
org.junit.jupiter.api.extension.ExtendWith; importar org.mockito.Mock; importar
org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
@Teste
void formatosGreetingWithName() {
quando(perfis.fetchNicknameFor(qualquer()))
.thenReturn("Alan");
String real =
saudação.formatGreeting(new UserId(""));
afirmarIsso(real)
Nesta seção, veremos um ótimo uso de objetos stub, que é sua função no teste de condições de erro.
À medida que criamos nosso código, precisamos garantir que ele lide bem com as condições de erro. Algumas condições de erro
são fáceis de testar. Um exemplo pode ser um validador de entrada do usuário. Para testar se ele lida com o erro causado por
dados inválidos, simplesmente escrevemos um teste que o alimenta com dados inválidos e, em seguida, escrevemos uma
asserção para verificar se foi relatado com sucesso que os dados eram inválidos. Mas e o código que o utiliza?
Se nosso SUT for um código que responde a uma condição de erro levantada por um de seus colaboradores, precisamos
testar essa resposta de erro. A forma como testamos depende do mecanismo que escolhemos para relatar esse erro.
Podemos estar usando um código de status simples; nesse caso, retornar esse código de erro de um stub funcionará muito bem.
Também podemos ter optado por usar exceções Java para relatar esse erro. As exceções são controversas. Se
usados incorretamente, eles podem levar a um fluxo de controle pouco claro em seu código. Precisamos saber como
testá-los, entretanto, pois eles aparecem em diversas bibliotecas Java e estilos de codificação internos. Felizmente,
não há nada difícil em escrever o teste para código de tratamento de exceções.
Começamos criando um esboço, usando qualquer uma das abordagens abordadas neste capítulo. Precisamos então
fazer com que o stub lance a exceção apropriada quando chamarmos um método. O Mockito tem um recurso
interessante para fazer isso, então vamos ver um exemplo de teste do Mockito que usa exceções:
@Teste
var notificações
= novo UserNotifications(mailServer);
assertThatExceptionOfType(NotificationFailureException.
aula)
.isThrownBy(()->notificações
Machine Translated by Google
No início deste teste, usamos Mockito doThrow() para configurar nosso objeto simulado. Isso configura o
objeto simulado mailServer do Mockito para lançar IllegalArgumentException sempre que chamamos
sendEmail(), não importa quais valores de parâmetro enviamos. Isso reflete uma decisão de design para
fazer sendEmail() lançar essa exceção como um mecanismo para relatar que o endereço de e-mail não era
válido. Quando nosso SUT chama mailServer.sendEmail(), esse método lançará IllegalArgumentExeption.
Podemos exercitar o código que lida com isso.
Podemos ver como isso é suficiente para acionar a resposta de tratamento de exceções em nosso código
SUT. Isso significa que podemos escrever nosso teste e então extrair o código necessário. O código que
escrevemos incluirá um manipulador catch para InvalidArgumentException. Nesse caso, tudo o que o novo
código precisa fazer é lançar um erro NotificationFailureException . Esta é uma nova classe que criaremos
que estende RuntimeException. Fazemos isso para informar que algo deu errado, enviando uma notificação.
Como parte das considerações normais de camadas do sistema, queremos substituir a exceção original por
uma mais geral, que seja mais adequada a esta camada de código.
Esta seção examinou recursos das bibliotecas Mockito e AssertJ que nos ajudam a usar TDD para eliminar o
comportamento de tratamento de exceções. Na próxima seção, vamos aplicar isso a um erro em nosso aplicativo Wordz.
Estaremos escrevendo uma classe WordSelection , que é responsável por escolher um número aleatório e
usá-lo para buscar uma palavra do armazenamento que esteja marcada com esse número. Estaremos
usando nossa interface RandomNumbers anterior. Neste exemplo, nosso teste cobrirá o caso em que
tentamos buscar uma palavra na interface do WordRepository , mas por algum motivo ela não está lá.
Machine Translated by Google
@ExtendWith(MockitoExtension.class)
@Zombar
@Zombar
@Teste
.quando(repositório)
.fetchWordByNumber(anyInt());
assertThatExceptionOfType(WordSelectionException.class)
.isThrownBy(
()->selection.getRandomWord());
O teste captura mais algumas decisões de design relacionadas a como pretendemos que o WordRepository e o
WordSelection funcionem. Nosso método de repositório fetchWordByNumber(wordNumber) lançará WordRepositoryException
se houver algum problema ao recuperar a palavra. Nossa intenção é fazer com que o WordSelection lance sua própria
exceção personalizada para relatar que não pode concluir a solicitação getRandomWord() .
Para configurar essa situação no teste, primeiro organizamos o lançamento do repositório. Isso é
feito usando o recurso Mockito doThrow() . Sempre que o método fetchWordByNumber() for
chamado, qualquer parâmetro que passarmos para ele, o Mockito lançará a exceção que
solicitamos, que é WordRepositoryException. Isso nos permite eliminar o código que trata essa condição de erro.
Machine Translated by Google
Resumo 147
Nossa etapa de organização é concluída com a criação da classe WordSelection SUT. Passamos dois
colaboradores para o construtor: a instância WordRepository e uma instância RandomNumbers . Pedimos
ao Mockito para criar stubs para ambas as interfaces adicionando a anotação @Mock para testar o dobro
do repositório e dos campos aleatórios .
Com o SUT agora devidamente construído, estamos prontos para escrever as etapas Act e Assert do
teste. Estamos testando se uma exceção é lançada, então precisamos usar assertThatExceptionOfType()
Facilidade AssertJ para fazer isso. Podemos passar a classe da exceção que esperamos lançar, que é
WordSelectionException. Encadeamos o método isThrownBy() para executar a etapa Act e fazer nosso código SUT ser
executado. Isso é fornecido como uma função Java lambda como parâmetro para o método isThrownBy() . Isso chamará
o método getRandomWord() , que pretendemos falhar e lançar uma exceção. A afirmação confirmará que isso aconteceu
e que o tipo esperado de classe de exceção foi lançado. Executaremos o teste, veremos sua falha e, em seguida,
adicionaremos a lógica necessária para fazer o teste passar.
O código de teste nos mostra que podemos usar testes duplos e verificação de condições de erro com TDD test-
first. Também mostra que os testes podem ser facilmente acoplados a uma implementação específica de uma solução.
Há muitas decisões de design neste teste sobre quais exceções acontecem e onde são usadas.
Essas decisões incluem até mesmo o fato de que exceções estão sendo usadas para relatar erros. Dito
isto , esta ainda é uma forma razoável de dividir responsabilidades e definir contratos entre componentes.
Tudo é capturado no teste.
Resumo
Neste capítulo, vimos como resolver o problema de testar colaboradores problemáticos. Aprendemos como usar objetos
substitutos para colaboradores chamados test doubles. Aprendemos que isso nos dá um controle simples sobre o que
esses colaboradores fazem dentro do nosso código de teste.
Dois tipos de teste duplo são especialmente úteis para nós: o stub e o mock. Stubs retornam dados. Os simulados
verificam se os métodos foram chamados. Aprendemos como usar a biblioteca Mockito para criar stubs e mocks para
nós.
Usamos AssertJ para verificar se o SUT se comportou corretamente sob as diversas condições de nossos testes duplos.
Aprendemos como testar condições de erro que geram exceções.
No próximo capítulo, abordaremos uma técnica de design de sistema muito útil que nos permite colocar a maior parte do
nosso código no PRIMEIRO teste de unidade e, ao mesmo tempo, evitar os problemas de testar colaborações com
sistemas externos que não podemos controlar.
Machine Translated by Google
Perguntas e respostas
1. Os termos stub e mock são usados de forma intercambiável?
Sim, embora tenham significados diferentes. Numa conversa normal, tendemos a trocar precisão por fluência, e tudo bem. É
importante entender os diferentes usos que cada tipo de teste duplo tem. Ao falar, geralmente é melhor não ser pedante sempre que
um grupo de pessoas sabe o que isso significa. Enquanto estivermos cientes de que um duplo de teste é o termo geral adequado e
que os tipos específicos de duplos têm funções diferentes, tudo ficará bem.
Isso acontece quando o SUT não possui lógica real, mas tentamos escrever um teste de unidade de qualquer maneira. Conectamos
um teste duplo ao SUT e escrevemos o teste. O que descobriremos é que as asserções apenas verificam se o teste retornou duas
vezes os dados corretos. É uma indicação de que testamos no nível errado. Esse tipo de erro pode ser causado pela definição de
metas de cobertura de código imprudentes ou pela força de uma regra de teste por método igualmente imprudente. Este teste não
Não. Isso só funciona se você projetou seu código usando o Princípio de Inversão de Dependência para que um teste duplo possa
ser trocado no lugar de um objeto de produção. Usar TDD certamente nos força a pensar sobre esse tipo de problema de design
desde o início.
Escrever testes posteriormente fica mais difícil se não houver acesso suficiente para injetar testes duplos onde eles são necessários.
O código legado é particularmente difícil nesse aspecto, e recomendo a leitura do livro Working Effectively with Legacy Code, de
Michael Feathers, para obter técnicas que ajudam a adicionar testes ao código que não possui os pontos de acesso de teste
Leitura adicional
• https://site.mockito.org/
Este livro explica como você pode trabalhar com código legado escrito sem pontos de acesso de inversão de dependência para testes
duplos. Ele mostra uma série de técnicas para retrabalhar com segurança o código legado para que testes duplos possam ser
introduzidos.
Machine Translated by Google
9
Arquitetura Hexagonal –
Desacoplando Sistemas Externos
Já aprendemos como escrever testes usando o modelo organizar, agir e afirmar. Também aprendemos alguns
princípios de design de software, conhecidos como princípios SOLID, que nos ajudam a dividir nosso software
em componentes menores. Finalmente, aprendemos como os testes duplos podem substituir componentes
colaborativos para tornar os testes de unidade FIRST mais fáceis de escrever. Neste capítulo, combinaremos
todas essas técnicas em uma poderosa abordagem de projeto conhecida como arquitetura hexagonal.
Usando essa abordagem, nos beneficiaremos ao obter mais lógica de nosso aplicativo em testes unitários e reduzir o número de
integração e testes ponta a ponta necessários. Construiremos uma resiliência natural a mudanças fora de nossa aplicação. Tarefas
de desenvolvimento, como mudar um fornecedor de banco de dados, serão simplificadas, tendo menos locais onde nosso código
precisa ser alterado. Também seremos capazes de realizar testes unitários em unidades maiores, trazendo alguns testes que
exigem testes ponta a ponta em outras abordagens em testes unitários.
Requerimentos técnicos
O código deste capítulo pode ser encontrado em https://github.com/PacktPublishing/
Desenvolvimento orientado a testes com Java/tree/main/chapter09.
Vejamos uma maneira simples de lidar com sistemas externos. A tarefa do nosso usuário é extrair um relatório das vendas
deste mês de um banco de dados. Escreveremos um trecho de código que faz exatamente isso. O design do software é
assim:
Neste design, temos dados de vendas armazenados em um banco de dados da maneira usual. Escrevemos algum código
para extrair o relatório em nome de nosso usuário. É um único trecho de código que faz todo o trabalho em uma única etapa.
Ele se conectará ao banco de dados, enviará uma consulta, receberá os resultados, fará algum processamento e formatará
os resultados para que o usuário os leia.
Do lado positivo, sabemos que esse estilo de codificação funciona. Ele atingirá seu objetivo de fornecer esse relatório de
vendas ao usuário. Por outro lado, o código combina três responsabilidades diferentes – acessar um banco de dados,
executar lógica e formatar um relatório. Pode misturar instruções SQL para o banco de dados com tags HTML5 para criar
um relatório formatado. Como vimos no capítulo anterior, isso pode fazer com que futuras alterações de código em uma
área se repercutam e impactem as outras áreas. Idealmente, isso não deveria acontecer. Mas o verdadeiro desafio é
escrever um teste para esse trecho de código. Precisaremos analisar e entender o formato em que enviamos o relatório ao
usuário. Também precisaremos trabalhar diretamente com esse banco de dados.
Nas subseções a seguir, revisaremos alguns desafios mais amplos que os sistemas externos apresentam aos testes. Isso
inclui problemas ambientais, transações acidentais, dados incertos, chamadas de sistema operacional e bibliotecas de
terceiros.
Machine Translated by Google
• A conexão de rede caiu: Vários motivos podem causar isso. Localmente, um cabo de rede foi retirado por engano. Talvez o banco de
dados esteja hospedado em algum lugar da Internet e nosso ISP tenha interrompido a conexão.
• Falhas de energia: Uma falha de energia no servidor de banco de dados ou em um switch de rede local é suficiente para
colocar o banco de dados fora do nosso alcance.
• Limites de equipamento: Talvez o próprio servidor de banco de dados tenha ficado sem espaço em disco e não consiga operar.
Talvez a consulta exata que escrevemos esteja atingindo o banco de dados de uma forma que leva muito tempo para ser concluída,
Seja qual for a causa, se nosso código não conseguir acessar os dados do banco de dados, ele não funcionará. Como essa é uma possibilidade,
escrever um teste para nosso código de geração de relatório fica muito mais difícil.
Mesmo quando nosso código pode acessar os dados do banco de dados, não é tão fácil trabalhar com ele nos testes.
Suponha que escrevemos um teste que verifica se podemos ler o banco de dados de produção corretamente, lendo um nome de usuário. Que
nome de usuário esperaríamos ler? Não sabemos, porque o teste não controla quais dados são adicionados. Os nomes de usuário disponíveis
Poderíamos fazer o teste adicionar um nome de usuário de teste conhecido ao banco de dados – mas acabamos de criar um usuário falso com o
qual usuários reais podem interagir. Não é isso que queremos.
Um banco de dados armazena dados, causando mais problemas para nossos testes. Suponha que escrevemos um teste em um banco de dados
de teste, que começa escrevendo um nome de usuário de teste. Se já executamos este teste antes, o nome de usuário do teste já estará
armazenado no banco de dados. Normalmente, o banco de dados reportará um erro de item duplicado e o teste falhará.
Os testes nos bancos de dados precisam ser limpos. Quaisquer dados de teste armazenados devem ser excluídos após a conclusão dos testes.
Se tentarmos excluir dados após o teste ter sido bem-sucedido, o código de exclusão poderá nunca ser executado se o teste falhar. Poderíamos
evitar isso sempre excluindo os dados antes da execução do teste. Esses testes serão lentos para serem executados.
O processador de pagamento pode emitir cobranças. Contas bancárias reais podem ser debitadas. Os alarmes podem ser ativados, causando
verdadeiras evacuações. Num famoso exemplo do Havai, um teste de sistema disparou uma mensagem de texto real dizendo que o Havai estava
sob ataque de mísseis – o que não era verdade. Isso é coisa séria.
Machine Translated by Google
Para obter detalhes sobre este exemplo de teste que deu errado, consulte https://en.wikipedia.org/
wiki/2018_Hawaii_false_missile_alert.
Transações reais acidentais podem resultar em perdas reais para uma empresa. Eles podem acabar como perdas para os
3Rs de uma empresa – receita, reputação e retenção. Nada disso é bom. Nossos testes não devem desencadear
acidentalmente consequências reais nos sistemas de produção.
Em nosso exemplo de relatório de vendas, o maior problema em escrever um teste é que precisaríamos
saber antecipadamente qual é a resposta correta para o relatório mensal de vendas. Como fazemos isso
quando estamos conectados ao sistema de produção? A resposta será o que o relatório de vendas disser.
Não temos outra maneira de saber.
O fato de precisarmos que o código do relatório de vendas funcione corretamente antes de podermos testar se o código do relatório
de vendas está funcionando corretamente é um grande problema aqui! Esta é uma dependência circular que não podemos quebrar.
Às vezes, nosso código pode precisar fazer chamadas ao sistema operacional para realizar seu trabalho. Talvez seja
necessário excluir todos os arquivos de um diretório de vez em quando ou pode depender da hora do sistema. Um exemplo
seria um utilitário de limpeza de arquivo de log, executado todas as segundas-feiras às 02h. O utilitário excluirá todos os
arquivos do diretório /logfiles/ .
Testar tal utilitário seria difícil. Teríamos que esperar até às 02h00 de segunda-feira e verificar se
todos os arquivos de log foram excluídos. Embora pudéssemos fazer isso funcionar, não é muito eficaz.
Seria bom encontrar uma abordagem melhor que nos permitisse testar sempre que quiséssemos, de preferência sem
excluir nenhum arquivo.
Uma tarefa comum em software empresarial é aceitar o pagamento de um cliente. Para isso, inevitavelmente utilizamos
um processador de pagamentos terceirizado como PayPal ou Stripe, como dois exemplos. Além dos desafios da
conectividade de rede, as APIs de terceiros nos oferecem outros desafios:
• Tempo de inatividade do serviço: muitas APIs de terceiros terão um período de manutenção programada em que
o serviço fica indisponível por um tempo. Isso significa “teste falhou” para nós.
• Mudanças na API: suponha que nosso código use a versão 1 da API e a versão 2 da API seja ativada.
Nosso código ainda usará chamadas da versão 1, que podem não funcionar mais na versão 2 da API.
Agora, isso é considerado uma prática bastante ruim – é chamado de quebrar uma interface publicada – mas pode
acontecer e acontece. Pior ainda, com nosso único trecho de código, as alterações da versão 2 podem causar
alterações em todo o nosso código.
Machine Translated by Google
• Respostas lentas: Se nosso código fizer uma chamada de API para um serviço externo, sempre existe a possibilidade
de que a resposta retorne mais tarde do que o esperado pelo nosso código. Nosso código normalmente falhará de
alguma forma e fará com que os testes falhem.
Existem muitos desafios quando misturamos serviços externos e um único código monolítico, complicando a manutenção e
os testes. A questão é o que podemos fazer sobre isso? A próxima seção analisa como o Princípio da Inversão de
Dependência pode nos ajudar a seguir uma abordagem de design conhecida como arquitetura hexagonal, que torna mais
fácil lidar com sistemas externos.
Aprendemos sobre o Princípio da Inversão de Dependência anteriormente neste livro. Vimos que isso nos
ajuda a isolar alguns códigos que queríamos testar dos detalhes de seus colaboradores. Observamos que
isso era útil para testar coisas conectadas a sistemas externos que estavam fora de nosso controle. Vimos
como o princípio da responsabilidade única nos guiou na divisão do software em tarefas menores e mais focadas.
Aplicando essas ideias ao nosso exemplo anterior de relatórios de vendas, chegaríamos a um design aprimorado, conforme
mostrado no diagrama a seguir:
O diagrama anterior mostra como aplicamos os princípios SOLID para dividir nosso código de relatório de vendas. Usámos o
princípio da responsabilidade única para dividir a tarefa global em três tarefas distintas:
• Formatando o relatório
Isso já torna o aplicativo um pouco mais fácil de trabalhar. Mais importante ainda, isolamos o código
que calcula o total de vendas do usuário e do banco de dados. Este cálculo não acessa mais
diretamente o banco de dados. Ele passa por outro trecho de código responsável por fazer apenas isso.
Da mesma forma, o resultado do cálculo não é formatado e enviado diretamente ao usuário. Outro pedaço de código é responsável por
isso.
• Podemos trocar qualquer trecho de código que possa acessar qualquer banco de dados
• Podemos usar testes duplos no lugar da formatação e do código de acesso ao banco de dados
O maior benefício é que podemos trocar qualquer trecho de código que possa acessar qualquer banco de dados, sem alterar
o código de cálculo. Por exemplo, poderíamos mudar de um banco de dados SQL Postgres para um banco de dados Mongo
Banco de dados NoSQL sem alterar o código de cálculo. Podemos usar um teste duplo para o banco de dados para que possamos
testar o código de cálculo como um PRIMEIRO teste de unidade. Estas são vantagens muito significativas, não apenas em termos de
TDD e testes, mas também em termos de como o nosso código é organizado. Considerando a solução completa de relatório de vendas
para esta, passamos da pura escrita de código para a engenharia de software. Estamos pensando além de apenas fazer o código
funcionar e nos concentrando em tornar o código fácil de trabalhar. As próximas subseções examinarão como podemos generalizar
essa abordagem, resultando na arquitetura hexagonal. Entenderemos como essa abordagem oferece uma organização lógica de código
que nos ajuda a aplicar o TDD de maneira mais eficaz.
O diagrama anterior mostra o que acontece quando generalizamos o uso da inversão de dependência e da
responsabilidade única para um aplicativo inteiro. É chamada de arquitetura hexagonal, também conhecida
como portas e adaptadores, em homenagem ao termo original usado por Alastair Cockburn, que primeiro
descreveu essa abordagem. A vantagem é que isola completamente a lógica central da nossa aplicação dos
detalhes dos sistemas externos. Isso nos ajuda a testar essa lógica central. Ele também fornece um modelo
razoável para um design bem projetado para nosso código.
• Sistemas externos, incluindo navegadores da web, bancos de dados e outros serviços de computação
• O modelo de domínio contém nossa lógica de aplicação, livre de detalhes externos do sistema
O núcleo central da nossa aplicação é o modelo de domínio, rodeado pelo suporte que necessita de sistemas externos.
Ele usa indiretamente, mas não é definido por esses sistemas externos. Vamos examinar cada componente da
arquitetura hexagonal com mais detalhes, para entender pelo que cada um é e pelo que não é responsável.
Machine Translated by Google
Sistemas externos são todas as coisas que vivem fora da nossa base de código. Eles incluem itens com os
quais o usuário interage diretamente, como o navegador da Web e o aplicativo de console no diagrama
anterior. Eles também incluem armazenamentos de dados, como o banco de dados SQL e o banco de dados NoSQL.
Outros exemplos de sistemas externos comuns incluem interfaces gráficas de usuário de desktop, sistemas de
arquivos, APIs de serviços web downstream e drivers de dispositivos de hardware. A maioria dos aplicativos precisará
interagir com sistemas como esses.
Na arquitetura hexagonal, o núcleo do código do nosso aplicativo não conhece nenhum detalhe sobre como os sistemas externos
interagem. A responsabilidade de se comunicar com sistemas externos
é fornecido a um trecho de código conhecido como adaptador.
Como exemplo, o diagrama a seguir mostra como um navegador da Web se conectaria ao nosso código por meio de um
Adaptador REST:
No diagrama anterior, podemos ver o navegador da web se conectando a um adaptador REST. Este adaptador entende solicitações
e respostas HTTP, que são o núcleo da web. Ele também entende o formato de dados JSON, geralmente usando bibliotecas para
converter os dados JSON em alguma representação interna para nosso código. Este adaptador também compreenderá o protocolo
específico que projetaremos para a API REST de nosso aplicativo – a sequência precisa de verbos HTTP, respostas, códigos de
status e dados de carga útil codificados em JSON que criamos como uma API.
Observação
Os adaptadores encapsulam todo o conhecimento que nosso sistema precisa para interagir com um sistema externo – e
nada mais. Este conhecimento é definido pelas especificações do sistema externo. Alguns deles podem ser projetados por
nós mesmos.
Os adaptadores têm a única responsabilidade de saber como interagir com um sistema externo. Se esse sistema externo alterar
sua interface pública, apenas nosso adaptador precisará ser alterado.
Machine Translated by Google
Seguindo em direção ao modelo de domínio, os adaptadores se conectam às portas. As portas fazem parte do modelo de domínio.
Eles abstraem os detalhes do conhecimento intrincado do adaptador sobre seu sistema externo. As portas respondem a
uma pergunta um pouco diferente: para que precisamos desse sistema externo? As portas usam o Princípio de Inversão
de Dependência para isolar nosso código de domínio do conhecimento de quaisquer detalhes sobre os adaptadores. Eles
são escritos puramente em termos do nosso modelo de domínio:
O adaptador REST descrito anteriormente encapsula os detalhes da execução de uma API REST, usando
conhecimento de HTTP e JSON. Ele se conecta a uma porta de comandos, que fornece nossa abstração de
comandos vindos da web – ou de qualquer outro lugar, nesse caso. Dado o nosso exemplo de relatório de
vendas anterior, a porta de comandos incluiria uma forma sem tecnologia de solicitar um relatório de vendas.
No código, pode parecer tão simples quanto isto:
pacote com.vendas.domínio;
importar java.time.LocalDate;
• O modificador de acesso público , para que possa ser chamado a partir do adaptador REST
Esta interface é uma porta. Isso nos fornece uma maneira geral de obter um relatório de vendas de nosso aplicativo.
Referindo-nos à Figura 9.3, podemos ver que o adaptador do console também se conecta a esta porta,
fornecendo ao usuário uma interface de linha de comando para nossa aplicação. A razão é que, embora os
usuários possam acessar nosso aplicativo usando diferentes tipos de sistemas externos – a web e a linha de
comando – nosso aplicativo faz a mesma coisa em ambos os casos. Ele suporta apenas um conjunto de
comandos, não importa de onde esses comandos sejam solicitados. Buscar um objeto SalesReport é
exatamente isso, não importa de qual tecnologia você o solicite.
Observação
As portas fornecem uma visão lógica do que nosso aplicativo precisa de um sistema externo, sem restringir como essas
necessidades devem ser atendidas tecnicamente.
As portas são onde invertemos as dependências. As portas representam a razão pela qual nosso modelo de domínio precisa
desses sistemas externos. Se os adaptadores representam o como, as portas representam o porquê.
A etapa final da cadeia é conectar-se ao próprio modelo de domínio. É aqui que reside a nossa lógica de aplicação. Pense nisso
como pura lógica para o problema que nosso aplicativo está resolvendo. Devido às portas e adaptadores, a lógica do domínio
não é limitada por detalhes de sistemas externos:
O modelo de domínio representa o que nossos usuários desejam fazer, em código. Cada história de usuário é descrita por código
aqui. Idealmente, o código nesta camada utiliza a linguagem do problema que estamos resolvendo, em vez de detalhes
tecnológicos. Quando fazemos isso bem, esse código se torna uma narrativa – ele descreve ações que interessam aos nossos
usuários nos termos que eles nos contaram. Ele usa a linguagem deles – a linguagem dos nossos usuários – e não uma
linguagem obscura de computador.
O modelo de domínio pode conter código escrito em qualquer paradigma. Pode usar programação funcional
(FP) ideias. Pode até usar ideias de programação orientada a objetos (OOP) . Pode ser processual.
Pode até usar uma biblioteca pronta para uso que configuramos declarativamente. Meu estilo atual é usar OOP
Machine Translated by Google
para a estrutura e organização geral de um programa e, em seguida, use as ideias de FP dentro dos métodos de objeto
para implementá-las. Não faz diferença para a arquitetura hexagonal ou para o TDD como implementamos esse modelo
de domínio. Qualquer que seja a forma que se adapte ao seu estilo de codificação, tudo bem aqui, desde que você use
as ideias de portas e adaptadores.
Observação
O modelo de domínio contém código que descreve como o problema do usuário está sendo resolvido. Esta é a
lógica essencial da nossa aplicação que cria valor comercial.
No centro de todo o aplicativo está o modelo de domínio. Ele contém a lógica que dá vida às histórias do usuário .
Quando nosso código segue essa abordagem de design, é simples verificar se as portas e os adaptadores estão divididos
corretamente. Podemos tomar duas decisões estruturais de alto nível:
Podemos analisar o código para verificar se algo no pacote de domínio não contém instruções de importação do pacote de
adaptadores . As verificações de importação podem ser feitas visualmente em revisões de código ou emparelhamento/mobbing.
Ferramentas de análise estática, como o SonarQube, podem automatizar as verificações de importação como parte do pipeline de construção.
O modelo de domínio nunca se conecta diretamente a nada na camada do adaptador, de modo que a lógica do
nosso aplicativo não depende de detalhes de sistemas externos.
Os adaptadores se conectam às portas para que o código conectado a sistemas externos seja isolado.
As portas fazem parte do modelo de domínio para criar abstrações de sistemas externos.
O modelo de domínio e os adaptadores dependem apenas das portas. Esta é a inversão de dependência em
ação.
Essas regras simples mantêm nosso design alinhado e preservam o isolamento do modelo de domínio.
Machine Translated by Google
A ideia por trás do formato hexágono usado no diagrama é que cada face representa um sistema externo.
Em termos de representação gráfica de um projeto, normalmente é suficiente ter até seis sistemas externos
representados . A ideia dos hexágonos internos e externos para representar o modelo de domínio e a camada
adaptadora mostra graficamente como o modelo de domínio é o núcleo de nossa aplicação e que está isolado
dos sistemas externos pelas portas e pela camada adaptadora.
A ideia crítica por trás da arquitetura hexagonal é a técnica de portas e adaptadores. O número
real de lados depende de quantos sistemas externos existem. O número deles não é importante.
Nesta seção, apresentamos a arquitetura hexagonal e os benefícios que ela oferece, além de fornecer uma visão geral de como todas
as peças essenciais se encaixam. Vamos para a próxima seção e examinaremos especificamente as decisões que precisamos tomar
para abstrair um sistema externo.
O lugar para começar nosso design é com nosso modelo de domínio. Precisamos criar uma porta adequada para o nosso modelo de
domínio interagir. Esta porta deve estar livre de quaisquer detalhes do nosso sistema externo e, ao mesmo tempo, deve responder à
questão de para que a nossa aplicação necessita deste sistema. Estamos criando uma abstração.
Uma boa maneira de pensar sobre abstrações é pensar sobre o que permaneceria igual se mudássemos a forma como executamos
uma tarefa. Suponha que queiramos comer uma sopa quente no almoço. Podemos aquecê-lo em uma panela no fogão ou talvez no
microondas. Não importa como escolhemos fazer isso, o que estamos fazendo permanece o mesmo. Estamos esquentando a sopa e
essa é a abstração que procuramos.
Não costumamos aquecer sopa em sistemas de software, a menos que estejamos construindo uma máquina automática de venda de
sopa. Mas existem vários tipos comuns de abstrações que usaremos. Isso ocorre porque tipos comuns de sistemas externos são
usados ao construir um aplicativo da web típico. A primeira e mais óbvia é a conexão com a própria web. Na maioria das aplicações,
encontraremos algum tipo de armazenamento de dados, normalmente um sistema de banco de dados de terceiros. Para muitos
aplicativos, também chamaremos outro serviço da web. Por sua vez, este serviço poderá acionar outros de uma frota de serviços, todos
internos à nossa empresa.
Outra chamada típica de serviço da Web é para um provedor de serviços da Web terceirizado, como um processador de pagamentos
com cartão de crédito, por exemplo.
Nosso aplicativo responderá a solicitações e respostas HTTP. A porta que precisamos projetar representa
a solicitação e a resposta em termos do nosso modelo de domínio, eliminando a tecnologia web.
Nosso exemplo de relatório de vendas poderia apresentar essas ideias como dois objetos de domínio simples. Essas
solicitações podem ser representadas por uma classe RequestSalesReport:
pacote com.vendas.domínio;
importar java.time.LocalDate;
classe • Os parâmetros dessa solicitação – ou seja, as datas de início e término do período do relatório
Esta é uma representação de modelo de domínio puro de uma solicitação de relatório de vendas entre duas datas.
Não há indícios de que isso tenha vindo da web, um fato que é muito útil, pois podemos solicitá-lo de outras fontes de
entrada, como uma GUI de desktop ou uma linha de comando. Melhor ainda, podemos criar esses objetos de modelo
de domínio facilmente em um teste de unidade.
O exemplo anterior mostra uma abordagem orientada a objetos, diga-não-pergunte. Poderíamos facilmente escolher uma
abordagem de FP. Se o fizéssemos, representaríamos a solicitação e a resposta como estruturas de dados puras. O recurso
de registro adicionado ao Java 17 é adequado para representar tais estruturas de dados. O importante é que a solicitação e
a resposta sejam escritas puramente em termos de modelo de domínio – nada da tecnologia web deve estar presente.
Sem dados, a maioria dos aplicativos não é particularmente útil. Sem armazenamento de dados, eles esquecem-se bastante
dos dados que fornecemos. Acessar armazenamentos de dados como bancos de dados relacionais e bancos de dados
NoSQL é uma tarefa comum no desenvolvimento de aplicações web.
Numa arquitetura hexagonal, começamos projetando a porta com a qual o modelo de domínio irá interagir, novamente em
termos de domínio puro. A maneira de criar uma abstração de banco de dados é pensar em quais dados precisam ser
armazenados e não como serão armazenados.
A interface costuma ser conhecida como repositório. Também foi denominado objeto de acesso a dados.
Seja qual for o nome, ele tem a função de isolar o modelo de domínio de qualquer parte do nosso banco de dados e
de sua tecnologia de acesso.
Existe um objeto de valor para transferir dados de um lugar para outro. Dois objetos de valor que contêm os mesmos
valores de dados são considerados iguais. Eles são ideais para transferir dados entre o banco de dados e nosso
código.
Voltando ao nosso exemplo de relatório de vendas, um design possível para o nosso repositório seria este:
pacote com.vendas.domínio;
}
Machine Translated by Google
Aqui, temos um método chamado allWithinDateRange() que nos permite buscar um conjunto de transações
de vendas individuais dentro de um determinado intervalo de datas. Os dados são retornados como java.util.List
de objetos simples de valor de venda . Estes são objetos de modelo de domínio completos. Eles podem muito
bem ter métodos que executam algumas das lógicas críticas do aplicativo. Eles podem ser pouco mais que
estruturas de dados básicas, talvez usando uma estrutura de registro Java 17 . Essa escolha faz parte do nosso
trabalho de decidir como será um projeto bem projetado em nosso caso específico.
• Detalhes da API JDBC ou JPA – a biblioteca padrão de conectividade de banco de dados Java
Nossos designs de repositório se concentram no que nosso modelo de domínio precisa que nosso banco de dados
forneça, mas não restringe a forma como ele fornece. Como resultado, algumas decisões interessantes devem ser
tomadas na concepção do nosso repositório, relativamente à quantidade de trabalho que colocamos na base de dados e
quanto fazemos no próprio modelo de domínio. Exemplos disso incluem decidir se escreveremos uma consulta complexa
no adaptador de banco de dados ou se escreveremos consultas mais simples e realizaremos trabalho adicional no modelo de domínio.
Da mesma forma, faremos uso de procedimentos armazenados no banco de dados?
Quaisquer que sejam as compensações que decidirmos nessas decisões, mais uma vez, o adaptador de banco de dados é onde residem
todas essas decisões. O adaptador é onde vemos as strings de conexão do banco de dados, strings de consulta, nomes de tabelas e assim
por diante. O adaptador encapsula os detalhes do design do nosso esquema de dados e tecnologia de banco de dados.
Fazer chamadas para outros serviços web é uma tarefa frequente de desenvolvimento. Os exemplos incluem chamadas para processadores
de pagamento e serviços de pesquisa de endereços. Às vezes, esses são serviços externos de terceiros e, às vezes, residem dentro de nossa
frota de serviços da web. De qualquer forma, eles geralmente exigem que algumas chamadas HTTP sejam feitas em nosso aplicativo.
A abstração dessas chamadas segue linhas semelhantes à abstração do banco de dados. Nossa porta é composta por uma interface que
inverte a dependência do serviço web que estamos chamando e alguns objetos de valor que transferem dados.
Um exemplo de abstração de uma chamada para uma API de mapeamento como o Google Maps, por exemplo, pode ser assim:
pacote com.vendas.domínio;
Temos uma interface que representa o MappingService como um todo. Adicionamos um método para adicionar
uma avaliação de um local específico em qualquer provedor de serviços que usarmos. Estamos usando
GeographicLocation para representar um lugar, definido em nossos termos. Pode muito bem ter um par de
latitude e longitude ou pode ser baseado no código postal. Essa é outra decisão de design. Novamente, não
vemos nenhum sinal do serviço de mapa subjacente ou de seus detalhes de API. Esse código reside no
adaptador, que se conectaria ao serviço web de mapeamento externo real.
Essa abstração nos oferece benefícios em poder usar um teste duplo para esse serviço externo e poder
mudar de provedor de serviço no futuro. Você nunca sabe quando um serviço externo pode ser encerrado
ou tornar-se muito caro para usar. É bom manter nossas opções abertas usando a arquitetura hexagonal.
Esta seção apresentou algumas ideias para as tarefas mais comuns no trabalho com sistemas externos em uma
arquitetura hexagonal. Na próxima seção, discutiremos abordagens gerais para escrever código no modelo de
domínio.
Nosso modelo de domínio é o núcleo de nossa aplicação e a arquitetura hexagonal o coloca em destaque . Um
bom modelo de domínio é escrito usando a linguagem do domínio do problema dos nossos usuários; é daí que
vem o nome. Deveríamos ver os nomes dos elementos do programa que nossos usuários reconheceriam .
Devemos reconhecer o problema que está a ser resolvido para além dos mecanismos que utilizamos para o
resolver. Idealmente, veremos termos de nossas histórias de usuários sendo usados em nosso modelo de domínio.
Aplicando a arquitetura hexagonal, escolhemos nosso modelo de domínio para ser independente daquilo que
não é essencial para a solução do problema. É por isso que os sistemas externos estão isolados. Podemos
inicialmente pensar que criar um relatório de vendas significa que devemos ler um arquivo e criar um documento HTML.
Mas esse não é o cerne essencial do problema. Precisamos simplesmente obter dados de vendas de algum lugar,
realizar alguns cálculos para obter os totais do nosso relatório e, em seguida, formatá-lo de alguma forma. O lugar
e de alguma forma podem mudar, sem afetar a essência da nossa solução.
Machine Translated by Google
Tendo esta restrição em mente, podemos adotar qualquer abordagem padrão de análise e projeto. Somos livres para escolher
objetos ou decompô-los em funções como normalmente fazemos. Basta-nos preservar a distinção entre a essência do problema
e os detalhes da implementação.
Precisamos exercer julgamento nessas decisões. Em nosso exemplo de relatório de vendas, a fonte dos dados de vendas não
tem importância. Como contra-exemplo, suponha que estejamos criando um linter para nossos arquivos de programa Java – é
bastante razoável ter o conceito de arquivos representado diretamente em nosso modelo de domínio.
Este domínio de problema trata de trabalhar com arquivos Java, portanto, devemos deixar isso claro. Ainda podemos dissociar
o modelo de domínio de um arquivo dos detalhes específicos do sistema operacional de leitura e gravação, mas o conceito
estaria no modelo de domínio.
O modelo de domínio pode usar qualquer biblioteca ou estrutura pré-escrita para ajudar a realizar seu trabalho. Bibliotecas
populares como Apache Commons ou Java Standard Runtime geralmente não apresentam problemas aqui. No entanto,
precisamos estar cientes das estruturas que nos ligam ao mundo dos sistemas externos e à nossa camada adaptadora.
Precisamos inverter as dependências desses frameworks, deixando-os apenas um detalhe de implementação da camada
adaptadora.
Um exemplo pode ser a anotação @RestController do Spring Boot. À primeira vista , parece um código
de domínio puro, mas vincula firmemente a classe ao código gerado específico do adaptador da web.
O modelo de domínio pode ser escrito usando qualquer paradigma de programação. Esta flexibilidade significa que teremos de
decidir qual a abordagem a utilizar. Esta nunca é uma decisão puramente técnica, como acontece com tantas coisas em
software. Devemos considerar o seguinte:
• Habilidades e preferências existentes da equipe: Qual paradigma a equipe conhece melhor? Qual
paradigma que gostariam de usar, se tivessem oportunidade?
• Bibliotecas, estruturas e bases de código existentes: se usarmos código pré-escrito – e vamos encarar os fatos, é
quase certo que o faremos – então qual paradigma seria melhor adequado para esse código?
• Guias de estilo e outras exigências de código: Estamos trabalhando com um guia de estilo ou paradigma existente?
Se estivermos sendo pagos pelo nosso trabalho – ou se estivermos contribuindo para um projeto de código aberto
existente – precisaremos adotar o paradigma que nos foi estabelecido.
A boa notícia é que qualquer que seja o paradigma que escolhermos, seremos capazes de escrever o nosso modelo de domínio
com sucesso. Embora o código possa parecer diferente, funcionalidades equivalentes podem ser escritas usando qualquer um
dos paradigmas.
Machine Translated by Google
Podemos ver que todos os adaptadores foram substituídos por duplos de teste, libertando-nos completamente do nosso ambiente
de sistemas externos. Os testes unitários agora podem cobrir todo o modelo de domínio, reduzindo a necessidade de testes de
integração.
• Podemos escrever testes TDD primeiro com facilidade: não há atrito em escrever um teste simples duplo
vive inteiramente na memória e não depende do ambiente de teste.
• Obtemos os benefícios do teste de unidade FIRST: nossos testes são executados muito rapidamente e podem ser
repetidos. Normalmente, testar um modelo de domínio inteiro leva alguns segundos, não horas. Os testes serão aprovados
ou reprovados repetidamente , o que significa que nunca nos perguntamos se uma falha de construção foi devido a uma
falha instável no teste de integração.
• Isso desbloqueia nossa equipe: podemos fazer um trabalho útil construindo a lógica central do nosso sistema, sem ter
que esperar que os ambientes de teste sejam projetados e construídos.
Machine Translated by Google
As técnicas para criar os testes duplos foram descritas no Capítulo 8, Testes duplos – Stubs e
Mocks. Não é necessário nada de novo em termos de implementação destas duplas.
Uma consequência de podermos testar todo o modelo de domínio é que podemos aplicar testes de unidade TDD e FIRST a
unidades de programa muito maiores. A próxima seção discute o que isso significa para nós.
A seção anterior introduziu a ideia de cercar nosso modelo de domínio com testes duplos para cada porta. Isso nos dá algumas
oportunidades interessantes para discutir nesta seção. Podemos testar unidades tão grandes quanto uma história de usuário.
Estamos familiarizados com os testes unitários como sendo coisas que testam em pequena escala. Há uma boa chance de você
ter ouvido alguém dizer que um teste de unidade só deve ser aplicado a uma única função, ou que cada classe deve ter um teste
de unidade para cada método. Já vimos como essa não é a melhor maneira de usar testes unitários. Testes como esses perdem
algumas vantagens. Ficaremos mais bem servidos se pensarmos nos testes como uma cobertura do comportamento.
A abordagem combinada de projetar com a arquitetura hexagonal e testar comportamentos em vez de detalhes de implementação
leva a uma interessante camada de sistema. Em vez de ter camadas tradicionais, como poderíamos fazer numa arquitetura de
três camadas, temos círculos de comportamento de nível cada vez mais elevado. Dentro do nosso modelo de domínio,
encontraremos esses testes pequenos. Mas à medida que avançamos para fora, em direção à camada adaptadora, encontraremos
unidades maiores de comportamento.
As portas no modelo de domínio formam um limite natural de alto nível do modelo de domínio. Se revisarmos o que aprendemos
neste capítulo, veremos que esse limite consiste no seguinte:
Essa camada é a essência do que nosso aplicativo faz, livre dos detalhes de como ele faz isso. Não é nada menos do que as
próprias histórias de usuários originais. A coisa mais significativa sobre este modelo de domínio é que podemos escrever os
PRIMEIROS testes de unidade nele. Temos tudo o que precisamos para substituir sistemas externos difíceis de testar por simples
testes duplos. Podemos escrever testes unitários que cubram histórias de usuários inteiras, confirmando que nossa lógica central
está correta.
Machine Translated by Google
Tradicionalmente, testar histórias de usuários envolvia testes de integração mais lentos em um ambiente de teste. A arquitetura
hexagonal permite que testes unitários substituam alguns desses testes de integração, acelerando nossas compilações e
proporcionando maior repetibilidade de nossos testes.
• Contra os comportamentos públicos de uma classe e de quaisquer colaboradores que ela tenha
Este é um grande benefício da arquitetura hexagonal. O isolamento de serviços externos tem o efeito de empurrar a lógica essencial de
uma história de usuário para o modelo de domínio, onde interage com as portas. Como vimos, essas portas – por design – são trivialmente
fáceis de escrever testes duplos. Vale a pena reafirmar os principais benefícios dos testes de unidade FIRST:
• Eles são muito rápidos, então testar nossas histórias de usuários será muito rápido
• Eles são altamente repetíveis, por isso podemos confiar em aprovações e falhas nos testes
À medida que cobrimos amplas áreas de funcionalidade com testes unitários, confundimos a linha entre integração e teste
unitário. Removemos o atrito dos desenvolvedores que testam mais histórias de usuários, tornando esses testes mais fáceis.
Usar mais testes de unidade melhora o tempo de construção, pois os testes são executados rapidamente e fornecem aprovação/
falhar nos resultados. São necessários menos testes de integração, o que é bom porque eles são executados mais lentamente e são mais
propensos a resultados incorretos.
Na próxima seção, aplicaremos o que aprendemos ao nosso aplicativo Wordz. Escreveremos uma porta que abstrai os detalhes da busca
apresentar a um usuário. Escreveremos os adaptadores e os testes de integração no Capítulo 14, Conduzindo a camada de banco de
dados.
• Pense no que o modelo de domínio precisa – por que precisamos desses dados? Para que será usado?
• Não faça simplesmente eco de uma implementação de banco de dados assumida – não pense em termos de tabelas e
chaves estrangeiras neste estágio. Isso acontecerá mais tarde, quando decidirmos como implementar o armazenamento.
Às vezes, as considerações de desempenho do banco de dados significam que temos que revisitar a abstração que
criamos aqui. Nós então trocaríamos o vazamento de alguns detalhes de implementação do banco de dados aqui se
isso permitisse que o banco de dados funcionasse melhor. Deveríamos adiar tais decisões o mais tarde possível.
• Considere quando devemos aproveitar mais o mecanismo de banco de dados. Talvez pretendamos usar procedimentos
armazenados complexos no mecanismo de banco de dados. Reflita essa divisão de comportamento na interface do repositório.
Pode sugerir uma abstração de nível superior na interface do repositório.
Para nosso exemplo de aplicação em execução, vamos considerar a tarefa de buscar uma palavra aleatoriamente para
o usuário adivinhar . Como devemos dividir o trabalho entre o domínio e o banco de dados? Existem duas opções amplas:
• Deixe o modelo de domínio gerar um número aleatório e deixe o banco de dados fornecer uma palavra numerada
Em geral, deixar o banco de dados trabalhar mais resulta em uma manipulação de dados mais rápida; o código do
banco de dados está mais próximo dos dados e não os arrasta por uma conexão de rede para nosso modelo de
domínio. Mas como persuadimos um banco de dados a escolher algo aleatoriamente? Sabemos que para bancos de
dados relacionais podemos emitir uma consulta que retornará resultados sem uma ordem garantida. Isso é meio
aleatório. Mas seria aleatório o suficiente? Em todas as implementações possíveis? Parece improvável.
A alternativa é deixar o código do modelo de domínio decidir qual palavra escolher, gerando um número aleatório.
Podemos então emitir uma consulta para buscar a palavra associada a esse número. Isso também sugere que cada
palavra tem um número associado a ela – algo que podemos fornecer quando projetarmos o esquema do banco de
dados posteriormente.
Esta abordagem implica que precisamos que o modelo de domínio escolha um número aleatório de todos os números
associados às palavras. Isso implica que o modelo de domínio precisa conhecer o conjunto completo de números para
escolher. Podemos tomar outra decisão de design aqui. Os números usados para identificar uma palavra começarão
em 1 e aumentarão em um para cada palavra. Podemos fornecer um método em nossa porta que retorne o limite
superior desses números. Então, estamos todos prontos para definir a interface do repositório – com um teste.
A classe de teste começa com a declaração do pacote e as importações da biblioteca que precisamos:
pacote com.wordz.domain;
importar org.junit.jupiter.api.BeforeEach;
importar org.junit.jupiter.api.Test;
importar org.junit.jupiter.api.extension.ExtendWith;
Machine Translated by Google
importar org.mockito.Mock;
importar org.mockito.MockitoAnnotations;
Habilitamos a integração do Mockito com uma anotação fornecida pela biblioteca junit-jupiter .
Adicionamos a anotação no nível da classe:
@ExtendWith(MockitoExtension.class)
Isso garantirá que o Mockito seja inicializado em cada execução de teste. A próxima parte do teste define algumas
constantes inteiras para facilitar a leitura:
Precisamos de dois testes duplos, que queremos que o Mockito gere. Precisamos de um esboço para o repositório
de palavras e um esboço para um gerador de números aleatórios. Devemos adicionar campos para esses stubs.
Marcaremos os campos com a anotação Mockito @Mock para que o Mockito gere os duplos para nós:
@Zombar
@Zombar
Mockito não vê diferença entre mock ou stub quando usamos a anotação @Mock . Ele simplesmente cria
um teste duplo que pode ser configurado para uso como simulação ou stub. Isso é feito posteriormente no
código de teste.
@Teste
void selectsWordAtRandom() {
quando(repositório.highestWordNumber())
.thenReturn(HIGHEST_WORD_NUMBER);
Machine Translated by Google
quando(repositório.fetchWordByNumber(WORD_NUMBER_SHINE))
.thenReturn("BRILHO");
quando(random.next(HIGHEST_WORD_NUMBER))
.entãoReturn(WORD_NUMBER_SHINE);
assertThat(real).isEqualTo("SHINE");
}
}
O teste anterior foi escrito da maneira normal, adicionando linhas para capturar cada decisão de projeto:
• A classe WordSelection encapsula o algoritmo, que seleciona uma palavra para adivinhar
• O método ChooseRandomWord() retornará uma palavra escolhida aleatoriamente como uma String
@BeforeEach
void beforeEachTest() {
quando(repositório.highestWordNumber())
.thenReturn(HIGHEST_WORD_NUMBER);
quando(repositório.fetchWordByNumber(WORD_NUMBER_SHINE))
.thenReturn("BRILHO");
}
Isso configurará os dados de teste no stub do nosso WordRepository no início de cada teste.
A palavra identificada pelo número 2 é definida como SHINE, então podemos verificar isso no assert.
Machine Translated by Google
pacote com.wordz.domain;
A interface WordRepository define a visão do banco de dados do nosso aplicativo. Precisamos apenas de duas
instalações para nossas necessidades atuais:
• Um método fetchWordByNumber() para buscar uma palavra, dado seu número de identificação
O teste também eliminou a interface necessária para nosso gerador de números aleatórios:
pacote com.wordz.domain;
pacote com.wordz.domain;
Resumo 173
int palavraNúmero =
random.next(repository.highestWordNumber());
Observe como este código não importa nada de fora do pacote com.wordz.domain .
É pura lógica de aplicação, contando apenas com as interfaces das portas para acessar palavras armazenadas e
números aleatórios. Com isso, nosso código de produção para o modelo de domínio do WordSelection está completo.
Resumo
Neste capítulo, aprendemos como aplicar os princípios SOLID para dissociar completamente os sistemas externos,
levando a uma arquitetura de aplicação conhecida como arquitetura hexagonal. Vimos como isso nos permite usar
testes duplos no lugar de sistemas externos, tornando nossos testes mais simples de escrever, com resultados
repetíveis. Isso, por sua vez, nos permite testar histórias de usuários inteiras com um PRIMEIRO teste de unidade.
Como bónus, isolamo -nos de alterações futuras nesses sistemas externos, limitando a quantidade de retrabalho
que seria necessário para suportar novas tecnologias. Vimos como a arquitetura hexagonal combinada com a
injeção de dependência nos permite suportar diversas opções de sistemas externos diferentes e selecionar aquele
que desejamos em tempo de execução por meio da configuração.
O próximo capítulo examinará os diferentes estilos de testes automatizados que se aplicam às diferentes
seções de uma aplicação de arquitetura hexagonal. Essa abordagem é resumida na Pirâmide de Teste, e
aprenderemos mais sobre ela lá.
Machine Translated by Google
Perguntas e respostas
Dê uma olhada nas seguintes perguntas e respostas sobre o conteúdo deste capítulo:
Nem sempre. Podemos refatorá-lo. O desafio pode ser o excesso de código que depende diretamente de
detalhes de sistemas externos. Se esse for o ponto de partida, essa refatoração será um desafio. Haverá
muito retrabalho a ser feito. Isto implica que é necessário algum grau de projeto inicial e discussão arquitetônica
antes de iniciarmos o trabalho.
Não. É uma forma de organizar dependências em nosso código. Pode ser aplicado a OOP, FP, programação
processual ou qualquer outra coisa – desde que essas dependências sejam gerenciadas corretamente.
Quando não temos lógica real em nosso modelo de domínio. Isso é comum para microsserviços CRUD muito
pequenos que normalmente fazem frontend de uma tabela de banco de dados. Sem lógica para isolar, inserir
todo esse código não traz nenhum benefício. Podemos também fazer TDD apenas com testes de integração e
aceitar que não poderemos usar os FIRST testes unitários.
Não. Muitas vezes é melhor termos mais portos. Suponha que tenhamos um único banco de dados Postgres
conectado ao nosso aplicativo, contendo dados sobre usuários, vendas e estoque de produtos. Poderíamos
simplesmente ter uma única interface de repositório, com métodos para trabalhar com esses três conjuntos
de dados. Mas será melhor dividir essa interface (seguindo o ISP) e ter UserRepository, SalesRepository e
InventoryRepository. As portas fornecem uma visão do que nosso modelo de domínio deseja dos sistemas
externos. As portas não são um mapeamento individual para o hardware.
Leitura adicional
Para saber mais sobre os tópicos abordados neste capítulo, dê uma olhada nos seguintes recursos:
• https://medium.com/pragmatic-programmers/unit-tests-are-first
rápido-isolado-repetível-auto-verificável-e-oportuno-a83e8070698e
Créditos aos inventores originais do termo FIRST, Tim Ottinger e Brett Schuchert.
• https://launchdarkly.com/blog/testing-in-production-for-safety
e-sanidade/
Guia para testar código implantado em sistemas de produção, sem desencadear consequências indesejadas
acidentalmente.
Machine Translated by Google
10
PRIMEIROS testes e o
Pirâmide de Teste
Até agora neste livro, vimos o valor de escrever testes unitários que sejam executados rapidamente e forneçam
resultados repetíveis. Chamados de testes FIRST, eles fornecem feedback rápido sobre nosso projeto. Eles são o
padrão ouro dos testes unitários. Também vimos como a arquitetura hexagonal nos ajuda a projetar nosso código de
uma forma que obtenha o máximo coberto pelos testes de unidade FIRST. Mas também nos limitamos a testar apenas
o nosso modelo de domínio – o núcleo da nossa lógica de aplicação. Simplesmente não temos testes que cubram
como nosso modelo de domínio se comporta quando se conecta ao mundo exterior.
Neste capítulo, cobriremos todos os outros tipos de testes de que precisamos. Apresentaremos a
pirâmide de testes, que é uma forma de pensar sobre os diferentes tipos de testes necessários e quantos
de cada devemos ter. Discutiremos o que cada tipo de teste cobre e técnicas e ferramentas úteis para ajudar.
Também reuniremos todo o sistema, introduzindo pipelines de CI/CD e ambientes de teste, descrevendo o papel crítico
que eles desempenham na combinação de componentes de código para criar um sistema para nossos usuários finais.
• A pirâmide de teste
• Testes de integração
Requerimentos técnicos
O código deste capítulo pode ser encontrado em https://github.com/PacktPublishing/
Test Driven-Development-with-Java/tree/main/chapter10.
Para executar este código, precisaremos instalar o banco de dados Postgres de código aberto localmente.
Machine Translated by Google
A pirâmide de teste
Uma maneira muito útil de pensar sobre diferentes tipos de testes é usar a pirâmide de testes. É uma
representação gráfica simples dos diferentes tipos de testes que precisamos em torno do nosso código e os
números relativos de cada um. Esta seção apresenta as ideias-chave por trás da pirâmide de teste.
Podemos ver no gráfico anterior que os testes estão divididos em quatro camadas. Temos testes de unidade na parte inferior. Os
testes de integração são colocados em camadas sobre eles. A pirâmide é completada por testes ponta a ponta e de aceitação do
usuário no topo. O gráfico mostra que os testes de unidade em nosso sistema são os mais numerosos, com menos testes de
integração e o menor número de testes de aceitação.
Alguns desses tipos de testes são novos neste livro. Vamos definir o que são:
• Testes unitários
Estes são familiares. São os PRIMEIROS testes que usamos até agora. Uma característica definidora desses testes é
que eles não exigem a presença de nenhum sistema externo, como bancos de dados ou processadores de pagamentos.
• Testes de integração
Esses testes verificam se um componente de software está corretamente integrado a um sistema externo, como um
banco de dados. Esses testes são lentos e dependem criticamente do ambiente externo disponível e configurado
corretamente para nosso teste.
Estes são os mais amplos de todos os testes. Um teste ponta a ponta representa algo muito próximo da experiência do
usuário final. Este teste é realizado em todos os componentes reais do sistema – possivelmente em ambientes de teste
com dados de teste – usando os mesmos comandos que um usuário real usaria.
É aqui que o sistema real é testado como um usuário o usaria. Aqui podemos confirmar que o sistema final é adequado
à finalidade, de acordo com os requisitos que o utilizador nos forneceu.
A princípio não é óbvio por que ter menos testes de qualquer tipo seria uma vantagem. Afinal, tudo até agora neste livro elogiou
positivamente o valor dos testes. Por que simplesmente não temos todos os testes? A resposta é pragmática: nem todos os testes
são criados iguais. Nem todos oferecem valor igual para nós, como desenvolvedores.
A razão para o formato desta pirâmide é refletir o valor prático de cada camada de teste. Os testes unitários escritos como testes
FIRST são rápidos e repetíveis. Se pudéssemos construir um sistema apenas com esses testes unitários, certamente o faríamos.
Mas os testes unitários não exercitam todas as partes da nossa base de código. Especificamente, eles não exercem conexões do
nosso código com o mundo exterior. Eles também não exercem nosso aplicativo da mesma forma que um usuário o usaria. À
medida que avançamos nas camadas de teste, deixamos de testar os componentes internos de nosso software e passamos a
testar como ele interage com sistemas externos e, em última análise, com o usuário final de nosso aplicativo.
A pirâmide de teste trata do equilíbrio. Seu objetivo é criar camadas de testes que alcançam o seguinte:
Nas seções a seguir, veremos uma análise dos testes envolvidos em cada camada do teste
pirâmide. Consideraremos os pontos fortes e fracos de cada tipo de teste, permitindo-nos entender para onde a pirâmide de
testes nos está guiando.
Nesta seção, veremos a base da pirâmide de testes, que consiste em testes unitários. Examinaremos por que essa camada é
crítica para o sucesso.
Até agora, estamos muito familiarizados com os testes unitários do FIRST. Os capítulos anteriores cobriram isso em detalhes.
Eles são o padrão ouro dos testes unitários. Eles são rápidos para correr. Eles são repetíveis e confiáveis. Eles são executados
isolados um do outro, então podemos executar um ou vários e executá-los na ordem que escolhermos.
Os testes FIRST são a força motriz do TDD, permitindo-nos trabalhar com um ciclo de feedback rápido enquanto codificamos.
Idealmente, todo o nosso código cairia nesse ciclo de feedback. Ele fornece uma maneira rápida e eficiente de trabalhar. A cada
passo, podemos executar o código e provar a nós mesmos que ele está funcionando conforme pretendido. Como um subproduto
útil, ao escrever testes que exercitam cada comportamento desejável possível em nosso código, acabaremos exercitando todos
os caminhos de código possíveis. Obteremos 100% de cobertura de teste significativa de código em testes de unidade quando
trabalharmos dessa maneira.
Devido às suas vantagens, os testes unitários constituem a base da nossa estratégia de testes. Eles são representados como a
base da pirâmide de teste.
Vantagens Limitações
Esses são os testes de execução mais rápida e fornecem O escopo menor desses testes significa que a aprovação em
o ciclo de feedback mais rápido possível para nosso código. todos os testes unitários não é garantia de que o sistema como
um todo esteja funcionando corretamente.
Estável e repetível, sem dependência de coisas fora do Eles podem ser escritos com uma ligação muito forte aos
nosso controle. detalhes de implementação, dificultando futuras adições e
refatorações.
Pode fornecer uma cobertura muito detalhada de um conjunto Não é útil para testar interações com sistemas
específico de lógica. Localize defeitos com precisão. externos.
Em qualquer sistema, esperamos ter o maior número de testes no nível da unidade. A pirâmide de teste representa isso
graficamente.
Machine Translated by Google
Não podemos obter cobertura total usando apenas testes unitários no mundo real, mas podemos melhorar a nossa
situação. Ao aplicar a arquitetura hexagonal ao nosso aplicativo, podemos obter a maior parte do código em testes
unitários. Nossos testes de unidade de execução rápida podem cobrir muitas áreas como essa e fornecer muita confiança
em nossa lógica de aplicação. Podemos chegar ao ponto de saber que se os sistemas externos se comportarem como
esperamos, nosso código da camada de domínio será capaz de lidar corretamente com todos os casos de uso que
pensamos.
Os testes unitários testam apenas componentes do nosso modelo de domínio. Eles não testam sistemas externos nem
usam sistemas externos. Eles contam com testes duplos para simular nossos sistemas externos para nós. Isto nos dá
vantagens na velocidade do ciclo de desenvolvimento, mas tem a desvantagem de que nossas conexões com esses
sistemas externos permanecem não testadas. Se tivermos um trecho de código testado em unidade que acessa uma
interface de repositório, sabemos que sua lógica funciona com um repositório stub. Sua lógica interna terá até 100% de
cobertura de teste e isso será válido. Mas ainda não saberemos se funcionará com o repositório real.
O código da camada do adaptador é responsável por essas conexões e não é testado no nível de teste de unidade. Para
testar essa camada, precisaremos de uma abordagem diferente de teste. Precisaremos testar o que acontece quando
nosso código da camada de domínio é integrado a sistemas externos reais.
A próxima seção analisa como testamos esses adaptadores de sistemas externos usando um tipo de teste conhecido
como testes de integração.
Testes de integração
Nesta seção, veremos a próxima camada da pirâmide de testes: testes de integração. Veremos por que isso é importante,
analisaremos ferramentas úteis e compreenderemos o papel dos testes de integração no esquema geral das coisas.
Existem testes de integração para testar se nosso código será integrado com sucesso a sistemas externos. Nossa
lógica principal de aplicação é testada por testes unitários, que, por design, não interagem com sistemas externos. Isso
significa que precisamos testar o comportamento desses sistemas externos em algum momento.
Machine Translated by Google
Os testes de integração são a segunda camada da pirâmide de testes. Eles apresentam vantagens e
limitações, conforme resumido na tabela a seguir:
Vantagens Limitações
Teste se os componentes de software interagem corretamente Exigir que ambientes de teste sejam configurados
quando conectado e mantido
Forneça uma simulação mais próxima do sistema de Os testes são executados mais lentamente que os testes de unidade
Deve haver menos testes de integração do que testes de unidade. Idealmente, muito menos. Embora os testes unitários tenham
evitado muitos problemas de teste de sistemas externos usando testes duplos, os testes de integração agora devem enfrentar
esses desafios. Por natureza, são mais difíceis de configurar. Eles podem ser menos repetíveis. Eles geralmente são executados
mais lentamente do que os testes unitários, pois aguardam respostas de sistemas externos.
Para dar uma ideia disso, um sistema típico pode ter milhares de testes unitários e centenas de testes de aceitação.
No meio, temos vários testes de integração. Muitos testes de integração apontam para uma oportunidade de design.
Podemos refatorar o código para que nosso teste de integração seja reduzido a um teste de unidade ou promovido a um teste de
aceitação.
Outro motivo para ter menos testes de integração são os testes instáveis. Um teste instável é um apelido
dado a um teste que às vezes passa e às vezes falha. Quando falha, é devido a algum problema de interação
com o sistema externo e não a um defeito no código que estamos testando. Tal falha é chamada de resultado
de teste falso negativo – um resultado que pode nos enganar.
Testes instáveis são um incômodo precisamente porque não podemos identificar imediatamente a causa raiz da falha.
Sem nos aprofundarmos nos logs de erros, sabemos apenas que o teste falhou. Isso faz com que os desenvolvedores aprendam a ignorar
esses testes que falharam, muitas vezes optando por executar novamente o conjunto de testes várias vezes até que o teste instável seja aprovado.
O problema aqui é que estamos treinando os desenvolvedores para terem menos fé em seus testes. Estamos treinando -os para
ignorar falhas nos testes. Este não é um bom lugar para se estar.
Este adaptador deve conter apenas a quantidade mínima de código necessária para interagir com o sistema
externo de uma forma que satisfaça nossa interface. Não deveria haver nenhuma lógica de aplicação. Isso deveria
Machine Translated by Google
estar dentro da camada de domínio e coberto por testes de unidade. Chamamos isso de adaptador fino, realizando apenas o
trabalho suficiente para se adaptar ao sistema externo. Isso significa que nosso teste de integração tem escopo bastante limitado.
Os testes de integração testam apenas os componentes da camada do adaptador, aqueles trechos de código que
interagem diretamente com sistemas externos, como bancos de dados e endpoints da web. O teste de integração criará
uma instância do adaptador em teste e fará com que ele se conecte a uma versão do serviço externo. Isto é importante.
Ainda não estamos nos conectando aos serviços de produção. Até que o teste de integração seja aprovado, não temos
certeza de que o código do nosso adaptador funciona corretamente. Portanto, ainda não queremos acessar serviços reais.
Também queremos ter esse nível extra de controle sobre esses serviços. Queremos ser capazes de criar contas de teste e dados falsos
com segurança e facilidade para usar com nosso adaptador. Isso significa que precisamos de uma coleção de serviços e bancos de
dados reais para usar. Isso significa que eles têm que viver e fugir para algum lugar.
Ambientes de teste é o nome dado ao arranjo de sistemas externos que utilizamos em testes de integração. É um ambiente para
execução de serviços web e fontes de dados, especificamente para testes.
Um ambiente de teste permite que nosso código se conecte a versões de teste de sistemas externos reais. Está
um passo mais perto da prontidão para produção, em comparação com o nível de teste unitário. No entanto,
existem alguns desafios envolvidos no uso de ambientes de teste. Vejamos as boas práticas para testar integrações
com bancos de dados e serviços web.
A abordagem básica para testar um adaptador de banco de dados é configurar um servidor de banco de dados no ambiente de teste e
fazer com que o código em teste se conecte a ele. O teste de integração pré-carregará um conjunto de dados conhecido no banco de
dados como parte da etapa de organização. O teste então executa o código que interage com o banco de dados na etapa Act. A etapa
Assert pode inspecionar o banco de dados para ver se ocorreram alterações esperadas no banco de dados.
O maior desafio em testar um banco de dados é que ele lembra os dados. Agora, isso pode parecer um pouco
óbvio, já que esse é o objetivo de usar um banco de dados em primeiro lugar. Mas entra em conflito com um dos
objetivos dos testes: ter testes isolados e repetíveis. Por exemplo, se nosso teste criou uma nova conta de usuário
Machine Translated by Google
para o usuário testuser1 e que estava armazenado no banco de dados, teríamos problemas ao executar esse
teste novamente. Não seria possível criar testuser1 e, em vez disso, receberia um erro de usuário já existente .
Existem diferentes abordagens para superar este problema, cada uma com vantagens e desvantagens:
• Excluir todos os dados do banco de dados antes e depois de cada caso de teste
Esta abordagem preserva o isolamento dos nossos testes, mas é lenta. Temos que recriar o esquema do banco de dados
de teste antes de cada teste.
• Exclua todos os dados antes e depois da execução do conjunto completo de testes do adaptador
Excluímos dados com menos frequência, permitindo que vários testes relacionados sejam executados no mesmo banco de
dados. Isso perde o isolamento do teste devido aos dados armazenados, pois o banco de dados não estará no estado
esperado no início do próximo teste. Temos que executar os testes em uma ordem específica, e todos eles devem passar,
para evitar prejudicar o estado do banco de dados para o próximo teste. Esta não é uma boa abordagem.
Em vez de criar testuser1 em nosso teste, randomizamos os nomes. Então, em uma execução, podemos
obter testuser-cfee-0a9b-931f. Na próxima execução, o nome de usuário escolhido aleatoriamente seria
outro. O estado armazenado no banco de dados não entrará em conflito com outra execução do mesmo
teste. Esta é outra maneira de preservar o isolamento do teste. No entanto, isso significa que os testes
podem ser mais difíceis de ler. Requer limpeza periódica do banco de dados de teste.
• Transações de reversão
Podemos adicionar os dados exigidos pelos nossos testes dentro de uma transação de banco de dados. Podemos reverter
a transação no final do teste.
• Ignore o problema
Às vezes, se trabalharmos com bancos de dados somente leitura, podemos adicionar dados de teste que nunca serão
acessados pelo código de produção e deixá-los lá. Se isto funcionar, é uma opção atraente que não requer nenhum esforço
extra.
Uma abordagem semelhante é usada para testar a integração com serviços web. Uma versão de teste do serviço web está
configurada para ser executada no ambiente de teste. O código do adaptador está configurado para se conectar a esta versão de
teste do serviço web, em vez da versão real. Nosso teste de integração pode então examinar como o código do adaptador se comporta.
Pode haver APIs web adicionais no serviço de teste para permitir a inspeção pelas afirmações em nosso teste.
Novamente, as desvantagens são um teste de execução mais lento e o risco de testes instáveis devido a problemas tão triviais
quanto o congestionamento da rede.
Machine Translated by Google
APIs de sandbox
Às vezes, hospedar nosso próprio serviço local pode ser impossível, ou pelo menos indesejável. Fornecedores
terceirizados geralmente não estão dispostos a lançar versões de teste de seus serviços para uso em nosso ambiente de teste.
Em vez disso, eles normalmente oferecem uma API sandbox. Esta é uma versão do serviço deles hospedada
por terceiros, não por nós. Está desconectado de seus sistemas de produção. Essa sandbox nos permite criar
contas e dados de teste, sem afetar nada real na produção. Ele responderá às nossas solicitações assim como
suas versões de produção responderão, mas sem realizar nenhuma ação, como receber pagamento. Considere
-os simuladores de teste para serviços reais.
O teste de contrato orientado ao consumidor envolve dois componentes, baseados nesse contrato, muitas vezes usando
código gerado por ferramentas. Isso está representado na figura a seguir:
O diagrama anterior mostra que capturamos as interações esperadas com um serviço externo
como um contrato de API. Nosso adaptador para esse serviço será codificado para implementar esse contrato de
API. Ao usar testes de contrato orientados ao consumidor, obtemos dois testes, que testam ambos os lados do contrato.
Se considerarmos um serviço como uma caixa preta, temos uma interface pública apresentada pela caixa preta e uma
implementação, cujos detalhes estão ocultos dentro dessa caixa preta. Um teste de contrato consiste em dois testes.
Um teste confirma que a interface externa é compatível com nosso código. O outro teste confirma que a implementação
daquela interface funciona e dá os resultados esperados.
Machine Translated by Google
• Um stub do serviço externo: É gerado um stub do serviço externo. Se estivermos chamando um processador de
.
pagamento, este stub simulará o processador de pagamento localmente. Isso nos permite usá-lo como um teste
duplo para o serviço de processamento de pagamentos enquanto escrevemos nosso código do adaptador.
Podemos escrever um teste de integração em nosso adaptador, configurando-o para chamar este stub. Isso nos
permite testar a lógica do código do adaptador sem acessar o sistema externo. Podemos verificar se o adaptador
envia as chamadas de API corretas para esse serviço externo e trata corretamente as respostas esperadas.
• Uma repetição de um conjunto de chamadas para o serviço externo real: O contrato também nos permite
executar testes no serviço externo real – possivelmente no modo sandbox. Não estamos testando a funcionalidade
do serviço externo aqui – presumimos que o provedor de serviços tenha feito isso. Em vez disso, estamos
verificando se o que acreditamos sobre sua API é verdade. Nosso adaptador foi codificado para fazer certas
chamadas de API em determinados pedidos. Este teste verifica se essa suposição está correta. Se o teste for
aprovado, sabemos que nosso entendimento da API do serviço externo estava correto e também que não mudou.
Se esse teste funcionasse anteriormente, mas agora falhar, isso seria uma indicação antecipada de que o serviço
externo alterou sua API. Precisaríamos então atualizar nosso código do adaptador para seguir isso.
Uma ferramenta recomendada para fazer isso é chamada Pact, disponível em https://docs.pact.io. Leia
os guias para obter mais detalhes sobre esta técnica interessante.
Vimos que os testes de integração nos aproximam um passo da produção. Na próxima seção, veremos o nível final de
testes na pirâmide de testes, que é o mais real até agora: testes de aceitação do usuário.
No topo da pirâmide de testes estão dois tipos semelhantes de testes, chamados testes ponta a ponta e testes de
aceitação do usuário . Tecnicamente, são o mesmo tipo de teste. Em cada caso, inicializamos o software totalmente
configurado para ser executado em seu ambiente de teste mais real, ou possivelmente em produção. A ideia é que o
sistema seja testado como um todo, de uma ponta à outra.
Um uso específico de um teste ponta a ponta é para testes de aceitação do usuário (UAT). Aqui, vários cenários
importantes de teste ponta a ponta são executados. Se todos forem aprovados, o software será declarado adequado à
finalidade e aceito pelos usuários. Muitas vezes, esta é uma fase contratual no desenvolvimento comercial, onde o
comprador do software concorda formalmente que o contrato de desenvolvimento foi cumprido. Ainda são testes de ponta
a ponta que estão sendo usados para determinar isso, com casos de teste escolhidos a dedo.
Vantagens Limitações
sistema e ainda assim ter esses testes nos protegendo. de “fragilidade” – nossos testes são altamente dependentes
do funcionamento correto do ambiente. Os ambientes
podem ser danificados devido a circunstâncias fora do nosso controle.
Contratualmente importantes – esses testes são a Esses são os testes mais desafiadores de escrever, devido
essência daquilo que preocupa o usuário final. aos extensos requisitos de configuração do ambiente.
Os testes de aceitação no topo da pirâmide são um reflexo de que não precisamos de muitos deles. A maior parte do
nosso código agora deve ser coberta por testes unitários e de integração, garantindo-nos que a lógica da nossa aplicação
funciona, bem como as nossas conexões com sistemas externos.
A questão óbvia é o que resta para testar? Não queremos duplicar testes que já foram feitos nos níveis de
unidade e integração. Mas precisamos de alguma forma de validar se o software como um todo funcionará
conforme o esperado. Este é o trabalho do teste ponta a ponta. É aqui que configuramos nosso software para
que ele se conecte a bancos de dados reais e serviços externos reais. Nosso código de produção passou em
todos os testes de unidade com testes duplos. Esses testes sugerem que nosso código deve funcionar quando
conectarmos esses serviços externos reais. Mas deveria é uma palavra maravilhosa no desenvolvimento de software.
Agora é a hora de verificar se isso acontece, usando um teste ponta a ponta. Podemos representar a cobertura desses
testes usando o seguinte diagrama:
Figura 10.6 – Testes de aceitação ponta a ponta/do usuário cobrem toda a base de código
Machine Translated by Google
Os testes ponta a ponta cobrem toda a base de código, tanto o modelo de domínio quanto a camada adaptadora.
Como tal, repete o trabalho de teste já realizado por testes unitários e de integração. O principal aspecto técnico
que queremos testar nos testes ponta a ponta é se nosso software está configurado e conectado corretamente.
Ao longo deste livro, usamos inversão e injeção de dependência para nos isolar de sistemas externos. Criamos
testes duplos e os injetamos. Agora, devemos criar o código de produção real, os componentes reais da camada
adaptadora que se conectam aos sistemas de produção. Nós os injetamos em nosso sistema durante sua
inicialização e configuração. Isso configura o código para funcionar de verdade.
Os testes ponta a ponta duplicarão uma pequena quantidade de testes do caminho feliz já cobertos pelos testes de unidade
e integração. O objetivo aqui não é verificar os comportamentos que já testamos. Em vez disso, estes testes verificam se
injetamos os objetos de produção corretos, confirmando que o sistema como um todo se comporta corretamente quando
conectado aos serviços de produção.
Um teste de aceitação do usuário baseia-se nessa ideia, executando os principais cenários de teste considerados críticos
para aceitar o software como completo. Serão testes ponta a ponta em nível técnico. Mas o seu propósito é mais amplo do
que o objetivo técnico de garantir que o nosso sistema esteja configurado corretamente. São mais de natureza jurídico-
contratual: Construímos o que nos foi pedido? Ao usar a abordagem iterativa deste livro juntamente com suas práticas
técnicas, há uma maior chance de que o tenhamos feito.
Existem várias bibliotecas de testes para nos ajudar a escrever testes automatizados de aceitação e de ponta a ponta.
Tarefas como conectar-se a um banco de dados ou chamar uma API web HTTP são comuns nesse tipo de teste. Podemos
aproveitar bibliotecas para essas tarefas, em vez de escrevermos nós mesmos o código.
O principal diferencial entre essas ferramentas é a forma como elas interagem com nosso software. Alguns têm como objetivo simular
um usuário clicando em uma GUI de desktop ou em uma interface de usuário da web baseada em navegador. Outros farão chamadas
HTTP para nosso software, exercendo um endpoint web.
• Descanse Fácil
Outra ferramenta popular para testar APIs REST que adota uma abordagem fluente para inspecionar
respostas JSON: https://rest-assured.io/
• Selênio
Uma ferramenta popular para testar UIs da web por meio do navegador: https://www.selenium.dev/
• Pepino
Os testes de aceitação formam a peça final da pirâmide de testes e permitem que nosso aplicativo seja testado
em condições semelhantes às do ambiente de produção. Tudo o que é necessário é uma forma de automatizar
executando todas essas camadas de testes. É aí que entram os pipelines de CI/CD, e eles são o assunto da próxima
seção.
Integração é onde pegamos componentes de software individuais e os juntamos para formar um todo. CI
significa que fazemos isso o tempo todo enquanto escrevemos um novo código.
Abordaremos a diferença mais tarde, mas em ambos os casos, a ideia é pegarmos a melhor e mais recente
versão do nosso software integrado e entregá-la às partes interessadas. O objetivo da entrega contínua é que
poderíamos – se quiséssemos – implantar cada alteração de código na produção com um único clique de um
botão.
É importante observar que CI/CD é uma disciplina de engenharia – não um conjunto de ferramentas. Seja como for , o
CI/CD tem o objetivo de desenvolver um sistema único que esteja sempre em estado utilizável.
Em termos de pirâmide de testes, o motivo pelo qual precisamos de CI/CD é reunir todos os testes. Precisamos de um mecanismo
para construir todo o nosso software, usando o código mais recente. Precisamos executar todos os testes e garantir que todos
sejam aprovados antes de podermos empacotar e implantar o código. Se algum teste falhar, sabemos que o código não é adequado
para implantação. Para garantir que obteremos feedback rápido, devemos executar os testes na ordem do mais rápido para o mais
lento. Nosso pipeline de CI executará testes de unidade primeiro, seguidos de testes de integração, seguidos de testes ponta a
ponta e de aceitação. Se algum teste falhar, a compilação produzirá um relatório de falhas de teste para esse estágio e, em seguida,
interromperá a compilação. Se todos os testes forem aprovados, empacotamos nosso código pronto para implantação.
De forma mais geral, a ideia de integração é fundamental para a construção de software, quer trabalhemos sozinhos
ou em equipe de desenvolvimento. Ao trabalharmos sozinhos, seguindo as práticas deste livro, estamos construindo
software a partir de vários blocos de construção. Alguns nós mesmos criamos, enquanto outros selecionamos um
componente de biblioteca adequado e o usamos. Também escrevemos adaptadores – componentes que nos permitem
Machine Translated by Google
para acessar sistemas externos. Tudo isso precisa ser integrado – reunido como um todo – para transformar nossas
linhas de código em um sistema funcional.
Ao trabalhar em equipe, a integração é ainda mais importante. Precisamos não apenas reunir as peças
que escrevemos, mas também todas as outras peças escritas pelo resto da nossa equipe. A integração
do trabalho em andamento dos colegas é urgente. Acabamos construindo sobre o que outros já escreveram.
Como trabalhamos fora da base de código principal integrada, existe o risco de não incluir as decisões de design
mais recentes e partes de código reutilizáveis.
A motivação por trás da CI era evitar a armadilha clássica do desenvolvimento em cascata, onde uma equipe escrevia
código como indivíduos isolados enquanto seguia um plano e só o integrava no final. Muitas vezes, essa integração
falhou em produzir software funcional. Muitas vezes havia algum mal-entendido ou falta de peças que significavam
que os componentes não se encaixavam. Nesta fase final de um projeto em cascata, os erros são caros para corrigir.
Não são apenas grandes equipes e grandes projetos que sofrem com isso. Meu ponto de virada foi enquanto escrevia
um jogo de simulador de vôo para a equipe de exibição RAF Red Arrows da Grã-Bretanha. Dois de nós trabalhamos
naquele jogo com uma API comum que havíamos combinado. Quando tentamos integrar nossas peças pela primeira
vez – às 03h00, na frente do diretor administrativo da empresa, é claro – o jogo rodou por cerca de três frames e
depois travou. Ops! Nossa falta de CI proporcionou uma lição embaraçosa. Teria sido bom saber que isso aconteceria
muito antes, especialmente sem a observação do diretor-gerente.
Machine Translated by Google
Entregar um fluxo de valor aos usuários finais é um princípio fundamental do desenvolvimento ágil. Não importa qual sabor
Da metodologia ágil que você usa, colocar os recursos nas mãos dos usuários sempre foi o objetivo.
Queremos fornecer recursos utilizáveis em intervalos regulares e curtos. Fazer isso oferece três benefícios:
Os usuários finais não se importam com nosso processo de desenvolvimento. Eles só se preocupam em obter
soluções para seus problemas. Seja esse o problema de se divertir enquanto espera por uma corrida no Uber ou
o problema de pagar os salários de todos em uma empresa multinacional, nosso usuário só quer que seu
problema desapareça. Obter recursos valiosos para nossos usuários torna-se uma vantagem competitiva.
Sim, foi isso que eu pedi – mas não foi isso que eu quis dizer! Esse é um feedback do usuário extremamente
valioso que as abordagens ágeis oferecem. Depois que um usuário final vê o recurso conforme o implementamos,
às vezes fica claro que ele não está resolvendo o problema. Podemos corrigir isso rapidamente.
Para realizar essa façanha, você precisa ter sua equipe e seus fluxos de trabalho juntos. Você não pode fazer
isso efetivamente, a menos que seu fluxo de trabalho resulte na disponibilidade contínua de software conhecido
como um todo.
Machine Translated by Google
• Entrega contínua
Entregamos software às partes interessadas internas, como proprietários de produtos e engenheiros de controle de qualidade
• Implantação contínua
Dos dois, a implantação contínua estabelece um padrão muito mais alto. Isso exige que, uma vez integrado o código em
nosso pipeline, esse código esteja pronto para entrar em operação – em produção, para usuários reais. É claro que isso é
difícil. Ele precisa de automação de testes de primeira classe para nos dar a confiança de que nosso código está pronto para
implantação. Ele também se beneficia por ter um sistema de reversão rápido em produção – um meio de reverter rapidamente
uma implantação se descobrirmos um defeito não coberto por nossos testes. A implantação contínua é o fluxo de trabalho
definitivo. Para todos que conseguem isso, implantar o novo código na última sexta-feira simplesmente não traz medo. Bem,
talvez um pouco menos de medo.
1. Controle de origem: Ter um local comum para armazenar o código é essencial para CI/CD.
É o lugar onde o código é integrado. O pipeline começa aqui, baixando a versão mais recente do código-fonte e
executando uma construção limpa. Isso evita erros causados pela presença de versões mais antigas de código no
computador.
2. Construção: Nesta etapa, executamos um script de construção para baixar todas as bibliotecas necessárias, compilar
todo o código e vinculá-lo. A saída é algo que pode ser executado, normalmente um único arquivo .jar Java , para
ser executado na JVM.
Machine Translated by Google
3. Análise estática de código: Linters e outras ferramentas de análise verificam o código-fonte em busca de violações estilísticas,
como comprimento variável e convenções de nomenclatura. A equipe de desenvolvimento pode optar por falhar na construção
quando problemas específicos de código forem detectados pela análise estática.
4. Testes de unidade: Todos os testes de unidade são executados no código construído. Se algum falhar, o pipeline será interrompido. Teste
5. Testes de integração: Todos os testes de integração são executados no código construído. Se algum falhar, o pipeline será
interrompido e mensagens de erro serão relatadas.
6. Testes de aceitação: Todos os testes de aceitação são executados no código construído. Se todos os testes passarem, o código
é considerado funcionando e pronto para entrega/implantação.
7. Embalagem de entrega: O código é embalado em um formato adequado para entrega. Para web Java
serviços, este pode muito bem ser um único arquivo .jar Java contendo um servidor web incorporado.
O que acontece a seguir depende das necessidades do projeto. O código empacotado pode ser implantado na produção
automaticamente ou pode simplesmente ser colocado em algum repositório interno, para acesso dos proprietários do produto e
engenheiros de controle de qualidade. A implantação formal aconteceria mais tarde, após a gestão da qualidade.
Ambientes de teste
Um problema óbvio causado pela necessidade de um pipeline de CI para executar testes de integração é ter um local para executar
esses testes. Normalmente, em produção, nosso aplicativo se integra a sistemas externos, como bancos de dados e provedores de
pagamento. Quando executamos nosso pipeline de CI, não queremos que nosso código processe pagamentos ou grave em bancos
de dados de produção. No entanto, queremos testar se o código pode ser integrado a essas coisas, uma vez configurado para se
conectar a esses sistemas reais.
A solução é criar um ambiente de teste. São coleções de bancos de dados e sistemas externos simulados que estão
sob nosso controle. Se nosso código precisar ser integrado a um banco de dados de detalhes do usuário, podemos
criar uma cópia desse banco de dados do usuário e executá-lo localmente. Durante o teste, podemos fazer com que
nosso código se conecte a esse banco de dados local, em vez da versão de produção. Provedores de pagamento
externos geralmente fornecem uma API sandbox. Esta é uma versão do seu serviço que, novamente, não se conecta
a nenhum dos seus clientes reais. Possui comportamento simulado para seu serviço. Na verdade, é um teste duplo externo.
Esse tipo de configuração é chamado de ambiente ao vivo ou de teste . Ele permite que nosso código seja testado com uma
integração mais realista. Nossos testes unitários usam stubs e mocks. Nossos testes de integração agora podem usar esses
ambientes de teste mais avançados.
Vantagens Desafios
O ambiente é independente Não ambientes de produção
Podemos criá-lo e destruí-lo à vontade. Não afetará Não importa o quão vivos os tornemos, esses
os sistemas de produção.
ambientes são simulações. O risco é que o nosso
ambientes falsos nos dão falsos positivos – testes que passam
apenas porque usam dados falsos. Isso pode nos dar uma falsa
confiança, levando-nos a implantar código que falhará na produção.
O ambiente nos deixa um passo mais perto dos testes É necessário mais trabalho de desenvolvimento para configurar
sob cargas e condições de produção. esses ambientes e mantê-los em sintonia com o código de teste.
Ambientes sandbox de terceiros nos permitem Simplesmente copiar um pedaço de dados de produção não é
confirmar se nosso código usa a API correta e mais suficiente para um ambiente de teste. Se esses dados contiverem
recente, conforme publicada pelo fornecedor. informações de identificação pessoal (PII) , conforme definido
pelo GDPR ou HIPAA, não poderemos usá-los legalmente diretamente.
Temos que criar uma etapa extra para anonimizar esses
dados ou gerar dados de teste aleatórios pseudo-realistas.
Nenhum dos dois é trivial.
Teste em produção
Já posso ouvir os suspiros! Executar nossos testes em produção geralmente é uma péssima ideia. Nossos testes podem
apresentar pedidos falsos que nosso sistema de produção trata como reais. Talvez seja necessário adicionar contas de usuário
de teste, o que pode representar um risco à segurança. Pior ainda, por estarmos em fase de testes, há uma grande chance de
nosso código ainda não funcionar. Isso pode causar todos os tipos de problemas – tudo isso enquanto estiver conectado aos
sistemas de produção.
Apesar dessas preocupações, às vezes, as coisas precisam ser testadas em produção. Empresas de big data como Google e
Meta têm coisas que só podem ser testadas ao vivo devido à grande escala de seus dados.
Não há como criar um ambiente de teste significativo e real; será simplesmente muito pequeno.
O que podemos fazer em casos como este?
A abordagem é mitigar os riscos. Duas técnicas são valiosas aqui: implantação azul-verde e particionamento de tráfego.
Machine Translated by Google
Implantação azul-verde
A implantação azul-verde é uma técnica de implantação projetada para a rápida reversão de implantações com falha.
Funciona dividindo os servidores de produção em dois grupos. Eles são chamados de azul e verde,
escolhidos por serem cores neutras que denotam sucesso. Nosso código de produção estará sendo
executado em um grupo de servidores por vez. Digamos que estamos atualmente no grupo azul.
Nossa próxima implantação será no grupo verde. Isso é mostrado no diagrama a seguir:
Depois que o código for implantado no grupo verde, alternamos a configuração de produção para conectar-se
aos servidores do grupo verde. Mantemos o código de produção anterior em funcionamento nos servidores azuis.
Se nossos testes correrem bem contra o grupo verde, estaremos prontos. A produção agora está trabalhando com o código
do grupo verde mais recente. Se o teste falhar, revertemos essa configuração para nos conectarmos novamente aos
servidores azuis. É um sistema de reversão rápido que permite nossa experimentação.
Particionamento de tráfego
Além da implantação azul-verde, podemos limitar a quantidade de tráfego que enviamos para nossos servidores de teste.
Em vez de inverter a produção para usar totalmente o novo código em teste, podemos simplesmente enviar uma pequena
porcentagem do tráfego do usuário para lá. Assim, 99% dos usuários podem ser roteados para nossos servidores azuis,
que sabemos que funcionam. 1% pode ser roteado para nosso novo código em teste nos servidores verdes, conforme
mostrado no diagrama a seguir:
Machine Translated by Google
Se defeitos forem descobertos, apenas 1% dos usuários serão afetados antes de revertermos para servidores 100% azuis.
Isso nos proporciona uma reversão rápida, mitigando problemas na produção causados por uma implantação com falha.
Já cobrimos as funções dos diferentes tipos de testes e vimos como eles se encaixam em um sistema coerente
conhecido como pirâmide de testes. Na próxima seção, aplicaremos parte desse conhecimento ao nosso
aplicativo Wordz escrevendo um teste de integração.
Nesta seção, revisaremos um teste de integração para nosso aplicativo Wordz para ter uma ideia de sua aparência .
Abordaremos os detalhes da criação desses testes e da configuração das ferramentas de teste no Capítulo 14, Conduzindo
a camada de banco de dados, e no Capítulo 15, Conduzindo a camada da Web.
A implementação desta interface WordRepository acessará o banco de dados e retornará uma palavra dada seu
wordNumber. Adiaremos a implementação disso para o Capítulo 14, Conduzindo a camada de banco de dados.
Por enquanto, vamos dar uma olhada antecipada em como será o teste de integração, em alto nível. O teste usa
bibliotecas de código aberto para ajudar a escrever o teste e fornecer o banco de dados. Escolhemos o seguinte:
• Postgres, um popular banco de dados relacional de código aberto, para armazenar nossos dados
pacote com.wordz.adapters.db;
importe com.github.database.rider.core.api.connection.
ConnectionHolder;
importar com.github.database.rider.core.api.dataset.DataSet; importar
com.github.database.rider.junit5.api.DBRider;
importar org.junit.jupiter.api.BeforeEach;
importar org.junit.jupiter.api.Test; importar
org.postgresql.ds.PGSimpleDataSource;
importar javax.sql.DataSource;
@DBRider
@BeforeEach
void beforeEachTest() {
var ds = new PGSimpleDataSource();
ds.setServerNames(new String[]{"localhost"}); ds.setDatabaseName("palavrazdb");
ds.setUser("ciuser");
ds.setPassword("cipassword");
this.dataSource = ds;
Machine Translated by Google
@Teste
@DataSet("adaptadores/data/wordTable.json")
public void buscaPalavra() {
var adaptador = novo WordRepositoryPostgres(dataSource);
assertThat(actual).isEqualTo("ARISE");
}
}
O método de teste fetchesWord() é marcado pela anotação @DataSet . Esta anotação é fornecida pela estrutura
de teste do banco de dados e constitui a etapa Organizar do nosso teste. Ele especifica um arquivo de dados de
teste conhecidos que a estrutura carregará no banco de dados antes da execução do teste. O arquivo de dados
está localizado abaixo da pasta raiz de src/test/resources. O parâmetro na anotação fornece o
restante do caminho. No nosso caso, o arquivo estará localizado em src/test/resources/adapters/
dados/wordTable.json. Seu conteúdo é assim:
{
"PALAVRA": [
{
"id": 1,
"número": 27,
"texto": "ARISE"
}
]
Este arquivo JSON informa à estrutura do banco de dados que gostaríamos de inserir uma única linha em uma
tabela de banco de dados chamada WORD, com valores de coluna 1, 27 e ARISE.
Ainda não vamos escrever o código do adaptador para fazer esse teste passar. Existem várias etapas que
precisaríamos seguir para compilar esse teste, incluindo o download de várias bibliotecas e a colocação do banco
de dados Postgres em funcionamento. Abordaremos essas etapas detalhadamente no Capítulo 14, Conduzindo a
camada de banco de dados.
Machine Translated by Google
Resumo 197
A visão geral deste código de teste de integração é que ele está testando uma nova classe chamada WordRepositoryPostgres que
iremos escrever. Essa classe conterá o código de acesso ao banco de dados.
Podemos ver o objeto JDBC revelador, javax.sql.DataSource, que representa uma instância de banco de dados.
Essa é a pista de que estamos testando a integração com um banco de dados. Podemos ver novas anotações
da biblioteca de testes de banco de dados: @DBRider e @DataSet. Finalmente, podemos ver algo
instantaneamente reconhecível – as etapas Organizar, Agir e Afirmar de um teste:
1. A etapa Organizar cria um objeto WordRepositoryPostgres , que conterá nosso código de banco de dados. Ele funciona com a
anotação @DataSet da biblioteca do banco de dados para colocar alguns dados conhecidos no banco de dados antes da
execução do teste.
3. A etapa Assert confirma que a palavra esperada, ARISE, foi retornada do banco de dados.
Como podemos ver, os testes de integração não são tão diferentes dos testes unitários em essência.
Resumo
Neste capítulo, vimos como a pirâmide de testes é um sistema que organiza nossos esforços de testes, mantendo os testes unitários
do FIRST firmemente como base para tudo o que fazemos, mas sem negligenciar outras preocupações de testes. Primeiro,
introduzimos as ideias de testes de integração e aceitação como formas de testar mais nosso sistema.
Em seguida, analisamos como as técnicas de CI e CD mantêm nossos componentes de software reunidos e prontos para lançamento
em intervalos frequentes. Vimos como reunir todo o processo de construção usando pipelines de CI, possivelmente passando para
CD. Fizemos um pequeno progresso no Wordz escrevendo um teste de integração para o adaptador WordRepositoryPostgres ,
preparando-nos para escrever o próprio código do banco de dados.
No próximo capítulo, daremos uma olhada na função dos testes manuais em nossos projetos. Está claro agora que automatizamos o
máximo de testes possível, o que significa que a função dos testes manuais não significa mais seguir enormes planos de testes. No
entanto, o teste manual ainda é muito valioso. Como o papel mudou? Analisaremos isso a seguir.
Perguntas e respostas
A seguir estão algumas perguntas e suas respostas em relação ao material deste capítulo:
A forma representa uma base ampla de muitos testes de unidade. Ele mostra camadas de testes acima que exercitam uma
maior aproximação ao sistema final integrado. Mostra também que esperamos menos testes nesses níveis mais elevados
de integração.
Testes unitários: rápidos e repetíveis. Não teste conexões com sistemas externos.
Machine Translated by Google
Testes de integração: Mais lentos, às vezes irrepetíveis. Eles testam a conexão com o sistema externo.
Testes de aceitação: os mais lentos de todos. Eles podem ser esquisitos, mas oferecem os testes mais
abrangentes de todo o sistema.
Não. Os testes só podem revelar a presença de um defeito, nunca a ausência de um. O valor de testes extensivos
está em quantos defeitos evitamos colocar em produção.
Não. Esta estratégia de cobertura de testes se aplica a qualquer paradigma de programação. Podemos escrever
código usando qualquer paradigma – orientado a objetos, funcional, processual ou declarativo. Os vários tipos de
testes dependem apenas de nosso código acessar sistemas externos ou constituir componentes puramente
internos.
5. Por que não preferimos testes ponta a ponta, já que eles testam todo o sistema?
Os testes ponta a ponta são executados lentamente. Eles dependem diretamente de ter bancos de dados de
produção e serviços da Web em execução ou de um ambiente de teste em execução contendo versões de teste
dessas coisas. As conexões de rede necessárias e coisas como configuração do banco de dados podem resultar
em testes que nos dão resultados falsos negativos. Eles falham por causa do ambiente, não porque o código
estava incorreto. Por esses motivos, projetamos nosso sistema para aproveitar ao máximo testes unitários rápidos
e repetíveis.
Leitura adicional
Para saber mais sobre os tópicos abordados neste capítulo, dê uma olhada nos seguintes recursos:
Pact.io produz uma ferramenta popular de teste de contrato de código aberto que está disponível
em seu site, https://docs.pact.io. O site apresenta um vídeo explicativo e uma introdução útil aos
benefícios dos testes orientados por contrato.
Uma biblioteca de teste de integração de banco de dados de código aberto que funciona com JUnit5. Ele
está disponível em https://database-rider.github.io/getting-started/.
Este livro explica detalhadamente as razões por trás do CD e várias práticas técnicas, como o desenvolvimento
baseado em trunk, para nos ajudar a conseguir isso. Altamente recomendado.
• CD mínimo
11
Explorando TDD com
Garantia da Qualidade
Os capítulos anteriores abordaram as práticas técnicas necessárias para projetar e testar códigos bem projetados. A
abordagem apresentada tem como objetivo principal que os desenvolvedores obtenham feedback rápido sobre o
design de software. Os testes têm sido quase um subproduto desses esforços.
A combinação de TDD, integração contínua e pipelines nos proporciona um alto nível de confiança em nosso código. Mas
eles não são tudo quando se trata de garantia de qualidade (QA) de software.
A criação de software da mais alta qualidade requer processos adicionais, com toque humano. Neste capítulo,
destacaremos a importância dos testes exploratórios manuais, das revisões de código, da experiência do usuário e
dos testes de segurança, juntamente com abordagens para adicionar um ponto de decisão humano a um lançamento de software.
TDD é uma disciplina relativamente recente no que diz respeito ao desenvolvimento mainstream. A gênese
moderna do TDD está com Kent Beck no Sistema de Compensação Abrangente da Chrysler (veja a seção
de leitura adicional, de onde veio a ideia do teste unitário do primeiro teste). O projeto começou em 1993 e
o envolvimento de Kent Beck começou em 1996.
O projeto Chrysler Comprehensive Compensation foi caracterizado pelo uso extensivo de testes unitários conduzindo pequenas
iterações e lançamentos frequentes de código. Esperamos que reconheçamos essas ideias dos capítulos anteriores deste livro. Muita
coisa mudou desde então – as opções de implantação são diferentes, o número de usuários aumentou e as abordagens ágeis são
mais comuns – mas os objetivos dos testes permanecem os mesmos. Esses objetivos são eliminar códigos corretos e bem projetados
e, em última análise, satisfazer os usuários.
A alternativa à automação de testes é executar testes sem automação – em outras palavras, executá-los manualmente. Um termo
melhor poderia ser orientado por humanos. Antes de a automação de testes se tornar comum, uma parte importante de qualquer
plano de desenvolvimento era um documento de estratégia de testes. Esses longos documentos definiam quando os testes seriam
feitos, como seriam feitos e quem os faria.
Este documento de estratégia existia juntamente com planos de teste detalhados. Também seriam documentos escritos, descrevendo
cada teste a ser realizado – como seria configurado, quais etapas exatamente deveriam ser testadas e quais deveriam ser os
resultados esperados. O projeto tradicional em cascata gastaria muito tempo definindo esses documentos. De certa forma, esses
documentos eram semelhantes ao nosso código de teste TDD, apenas escrito em papel, em vez de código-fonte.
A execução desses planos de teste manuais exigiu um grande esforço. A execução de um teste exige que configuremos os dados
de teste manualmente, executemos o aplicativo e, em seguida, cliquemos nas interfaces do usuário. Os resultados devem ser
documentados. Os defeitos encontrados devem ser registrados em relatórios de defeitos. Eles devem ser alimentados de volta em
cascata, desencadeando redesenhos e recodificações. Isso deve acontecer com cada lançamento. Os testes conduzidos por
humanos são repetíveis, mas apenas com um grande custo de preparação, atualização e acompanhamento dos documentos de
teste. Tudo isso levou tempo – e muito tempo.
Neste contexto, as ideias de Beck sobre TDD pareciam notáveis. Os documentos de teste tornaram-se códigos executáveis e podiam
ser executados quantas vezes fosse desejada, por uma fração do custo dos testes humanos. Esta foi uma visão convincente. A
responsabilidade de testar o código fazia parte do mundo do desenvolvedor agora. Os testes faziam parte do próprio código-fonte.
Esses testes foram automatizados, capazes de serem executados integralmente em cada compilação e mantidos atualizados
conforme o código mudava.
É tentador pensar que o uso do TDD conforme descrito neste livro pode eliminar os testes manuais. Elimina alguns processos
manuais, mas certamente não todos. As principais etapas manuais que substituímos pela automação são testes de recursos durante
o desenvolvimento e testes de regressão antes do lançamento.
À medida que desenvolvemos um novo recurso com TDD, começamos escrevendo testes automatizados para esse recurso. Cada teste
automatizado que escrevemos é um teste que não precisa ser executado manualmente. Economizamos todo o tempo de configuração do teste,
Machine Translated by Google
junto com o processo muitas vezes demorado de clicar em uma interface de usuário para acionar o comportamento que estamos
testando. A principal diferença que o TDD traz é a substituição dos planos de teste escritos em um processador de texto por código
de teste escrito em um IDE. O teste manual do recurso de desenvolvimento é substituído pela automação.
Usando TDD, adicionamos um ou mais testes à medida que construímos cada recurso. Significativamente, mantemos todos esses
testes. Naturalmente, construímos um grande conjunto de testes automatizados, capturados no controle de origem e executados
automaticamente em cada compilação. Isso é conhecido como conjunto de testes de regressão. O teste de regressão significa que
verificamos novamente todos os testes executados até o momento em cada construção. Isso garante que, à medida que fazemos
alterações no sistema, não quebramos nada. Agir rápido e não quebrar as coisas pode ser a forma como descrevemos essa abordagem.
Os testes de regressão também incluem testes para defeitos relatados anteriormente. Estes testes de regressão confirmam que
não foram reintroduzidos. Vale a pena repetir que o conjunto de regressão economiza todo o esforço manual exigido pelos testes
não automatizados toda vez que o conjunto é executado. Isso resulta em uma enorme redução ao longo do ciclo de vida completo
do software.
A automação de testes é boa, mas um teste automatizado é uma máquina de software. Ele não pode pensar por si mesmo.
Ele não pode inspecionar visualmente o código. Ele não pode avaliar a aparência de uma interface de usuário. Não é
possível dizer se a experiência do usuário é boa ou ruim. Não pode determinar se o sistema global é adequado à sua finalidade.
É aqui que entram os testes manuais conduzidos por humanos. As seções a seguir examinarão as áreas onde precisamos de testes
conduzidos por humanos, começando com uma óbvia: encontrar bugs que nossos testes não perceberam.
A maior ameaça ao nosso sucesso com o TDD reside na nossa capacidade de pensar em todas as condições que o nosso software
precisa suportar. Qualquer software razoavelmente complexo tem uma enorme variedade de combinações de entrada possíveis,
casos extremos e opções de configuração.
Machine Translated by Google
Considere usar TDD para escrever código para restringir as vendas de um produto a compradores com 18 anos ou mais.
Devemos primeiro escrever um teste do caminho feliz para verificar se a venda é permitida, fazê-la passar e, em seguida,
escrever um teste negativo, confirmando que a venda pode ser bloqueada com base na idade. Este teste tem o seguinte formato:
void saleRestrictedTo17yearOld() {
// ... código de teste omitido
@Teste
void salePermitedTo19yearOld() {
// ... código de teste omitido
}
}
O erro é óbvio quando o procuramos: o que acontece na fronteira entre as idades de 17 e 18 anos? Um jovem de 18
anos pode comprar este produto ou não? Não sabemos, porque não existe teste para isso.
Testamos para 17 e 19 anos. Aliás, o que deveria acontecer nessa fronteira? Em geral, essa é uma decisão das
partes interessadas.
• Pergunte a uma parte interessada o que ela deseja que o software faça
É aqui que entra o teste exploratório manual. Esta é uma abordagem de teste que aproveita ao máximo a criatividade
humana. Ele usa nossos instintos e inteligência para descobrir quais testes podemos estar perdendo. Em seguida,
utiliza experimentação científica para descobrir se as nossas previsões de um teste em falta estavam corretas. Se
isso for comprovado, podemos fornecer feedback sobre essas descobertas e reparar o defeito. Isso pode ser feito
como uma discussão informal ou usando uma ferramenta formal de rastreamento de defeitos. No devido tempo,
poderemos escrever novos testes automatizados para capturar nossas descobertas e fornecer testes de regressão para o futuro.
Esse tipo de teste exploratório é um trabalho altamente técnico, baseado no conhecimento dos tipos de limites
existentes nos sistemas de software. Também requer amplo conhecimento de implantação local e configuração de
sistemas de software, além de saber como o software é construído e onde é provável que os defeitos apareçam. Até
certo ponto, depende de saber como os desenvolvedores pensam e de prever os tipos de coisas que eles podem
ignorar.
Algumas diferenças importantes entre testes automatizados e testes exploratórios podem ser resumidas da seguinte forma:
Machine Translated by Google
Repetivel Criativo
Planejado Oportunista
Testes exploratórios manuais sempre serão necessários. Até mesmo os melhores desenvolvedores ficam sem
tempo, distraídos ou têm outra reunião que deveria ser por e-mail. Depois que a concentração é perdida, é muito
fácil que erros apareçam. Alguns testes ausentes estão relacionados a casos extremos que não podemos ver
sozinhos. Outra perspectiva humana muitas vezes traz uma nova visão que simplesmente nunca teríamos sem ajuda.
O teste exploratório manual fornece uma importante camada extra de defesa em profundidade contra defeitos que passam despercebidos.
Depois que o teste exploratório identificar algum comportamento inesperado, podemos realimentar isso no desenvolvimento.
Nesse ponto, podemos usar o TDD para escrever um teste para o comportamento correto, confirmar a presença do defeito e então
desenvolver a correção. Agora temos uma correção e um teste de regressão para garantir que o bug permaneça corrigido.
Podemos pensar nos testes exploratórios manuais como o ciclo de feedback mais rápido possível para um defeito que não percebemos.
Um excelente guia para testes exploratórios está listado na seção Leitura adicional.
Visto sob esta luz, os testes de automação e o TDD não tornam os esforços manuais menos importantes. Em vez disso, o seu valor é
amplificado. As duas abordagens trabalham juntas para agregar qualidade à base de código.
O teste manual para coisas que perdemos não é a única atividade de valor no tempo de desenvolvimento que não pode ser
automatizada. Temos também a tarefa de verificar a qualidade do nosso código-fonte, assunto da próxima seção.
Como vimos ao longo deste livro, o TDD está preocupado principalmente com o design do nosso código. À medida que construímos um
teste unitário, definimos como nosso código será utilizado por seus consumidores. A implementação desse projeto não é motivo de
preocupação para nossos testes, mas nos preocupa como engenheiros de software. Queremos que essa implementação tenha um bom
desempenho e seja fácil de entender para o próximo leitor. O código é lido muito mais vezes do que escrito durante seu ciclo de vida.
Existem algumas ferramentas automatizadas para ajudar na verificação da qualidade do código. Elas são conhecidas como ferramentas
de análise de código estático. O nome vem do fato de não executarem código; em vez disso, eles realizam um processo automatizado
Machine Translated by Google
revisão do código-fonte. Uma ferramenta popular para Java é o Sonarqube (em https://www.sonarqube.
org/), que executa um conjunto de regras em uma base de código.
• Vulnerabilidades de segurança
Essas regras podem ser modificadas e adicionadas, permitindo a personalização do estilo e das regras da casa do projeto local.
É claro que tais avaliações automatizadas têm limitações. Tal como acontece com os testes exploratórios manuais, existem
simplesmente algumas coisas que apenas um ser humano pode fazer (pelo menos no momento em que este artigo foi escrito). Em
termos de análise de código, isso envolve principalmente contextualizar as decisões. Um exemplo simples aqui é preferir nomes
de variáveis mais longos e descritivos a um primitivo como int, em comparação com um tipo mais detalhado como WordRepository.
As ferramentas estáticas carecem dessa compreensão dos diferentes contextos.
A análise automatizada de código tem seus benefícios e limitações, conforme resumido aqui:
Regras rígidas (por exemplo, comprimento de nome variável) Relaxa as regras com base no contexto
O Google possui um sistema muito interessante chamado Google Tricorder. Este é um conjunto de ferramentas de análise de
programas que combina a criatividade dos engenheiros do Google na elaboração de regras para um bom código com a automação
para aplicá-las. Para obter mais informações, consulte https://research.google/pubs/pub43322/.
A revisão manual do código pode ser feita de várias maneiras, com algumas abordagens comuns:
Uma solicitação pull, também conhecida como solicitação de mesclagem, é feita por um desenvolvedor quando deseja
integrar suas alterações de código mais recentes na base de código principal. Isso oferece uma oportunidade para outro
desenvolvedor revisar esse trabalho e sugerir melhorias. Eles podem até detectar defeitos visualmente. Assim que o
desenvolvedor original fizer as alterações acordadas, a solicitação será aprovada e o código será mesclado.
Machine Translated by Google
• Programação em pares:
A programação em pares é uma forma de trabalhar onde dois desenvolvedores trabalham na mesma tarefa ao mesmo
tempo. Há uma discussão contínua sobre como escrever o código da melhor maneira. É um processo de revisão
contínua. Assim que o desenvolvedor detecta um problema ou sugere uma melhoria, uma discussão acontece e uma
decisão é tomada. O código é continuamente corrigido e refinado à medida que é desenvolvido.
Assim como a programação em pares, apenas toda a equipe participa da escrita do código para uma tarefa. O máximo
em colaboração, que traz continuamente o conhecimento e as opiniões de uma equipe inteira para cada parte do
código escrito.
A diferença dramática aqui é que uma revisão de código acontece depois que o código é escrito, mas a programação em pares
e o mobbing acontecem enquanto o código está sendo escrito. As revisões de código realizadas após a escrita do código
geralmente acontecem tarde demais para permitir que alterações significativas sejam feitas. O emparelhamento e o mobbing
evitam isso revisando e refinando o código continuamente. As alterações são feitas no instante em que são identificadas. Isso
pode resultar em resultados de maior qualidade entregues mais cedo em comparação com o fluxo de trabalho de código e revisão.
Diferentes situações de desenvolvimento adoptarão práticas diferentes. Em todos os casos, adicionar um segundo par
de olhos humanos (ou mais) oferece uma oportunidade para uma melhoria no nível de design, não no nível de sintaxe.
Com isso, vimos como os desenvolvedores podem se beneficiar ao adicionar testes exploratórios manuais e revisão de
código ao seu trabalho de TDD. As técnicas manuais também beneficiam nossos usuários, como abordaremos na próxima seção.
• Permite que um usuário atinja seus objetivos finais de maneira eficaz e eficiente
O primeiro deles, que fornece funcionalidade, é o mais programático dos dois. Da mesma forma que usamos TDD para
gerar um bom design para nosso código do lado do servidor, também podemos usá-lo em nosso código frontend. Se
nosso aplicativo Java gera HTML – chamado de renderização do lado do servidor – o uso do TDD é trivial. Nós testamos o
Machine Translated by Google
Adaptador de geração de HTML e pronto. Se estivermos usando um framework JavaScript/Typescript rodando no navegador, podemos
usar TDD lá, com um framework de teste como o Jest (https://jestjs.io/).
Depois de testarmos se estamos fornecendo as funções corretas ao usuário, a automação se torna menos útil.
Com o TDD, podemos verificar se todos os tipos corretos de elementos gráficos estão presentes em nossa interface de usuário.
Mas não podemos dizer se eles atendem às necessidades do usuário.
Considere esta interface de usuário fictícia para comprar mercadorias relacionadas ao nosso aplicativo Wordz:
Podemos usar o TDD para testar se todos esses elementos da interface – as caixas e botões – estão presentes e funcionando.
Mas nossos usuários se importarão? Aqui estão as perguntas que precisamos fazer:
• Parece e é agradável?
De forma bastante deliberada para este exemplo, a resposta é não a todas estas questões. Francamente, este é um layout de
interface de usuário terrível. Não tem estilo, nem sentimento, nem identidade de marca. Você deve digitar o nome do produto no
campo de texto. Não há imagem do produto, nem descrição, nem preço! Essa interface de usuário é realmente a pior que se pode
imaginar para uma página de vendas de produtos de comércio eletrônico. Mesmo assim, passaria em todos os nossos testes
automatizados de funcionalidade.
Projetar interfaces de usuário eficazes é uma habilidade muito humana. Envolve um pouco de psicologia para saber
como o ser humano se comporta diante de uma tarefa, mesclado com um olhar artístico, respaldado pela criatividade.
Essas qualidades de uma interface de usuário são melhor avaliadas por humanos, acrescentando outra etapa manual ao nosso
processo de desenvolvimento.
Machine Translated by Google
A experiência do usuário vai além de qualquer elemento ou visualização individual em uma interface de usuário. É toda a
experiência que nossos usuários têm, de ponta a ponta. Quando queremos encomendar a última camiseta Wordz em nossa loja
de e-commerce, queremos que todo o processo seja fácil. Queremos que o fluxo de trabalho em todas as telas seja óbvio,
organizado e mais fácil de acertar do que errar. Indo além, o design de serviço visa otimizar a experiência, desde querer uma
camiseta até usá-la.
Garantir que os usuários tenham uma ótima experiência é o trabalho de um designer de experiência do usuário. É uma atividade
humana que combina empatia, psicologia e experimentação. A automação é limitada na forma como pode ajudar aqui. Algumas
partes mecânicas disso podem ser automatizadas. Candidatos óbvios são aplicativos como Invision (https://www.invisionapp.com/),
que nos permite produzir uma maquete de tela com a qual podemos interagir, e Google Forms, que nos permite coletar feedback
pela web, sem código para configurar isso.
Depois de criar uma experiência de usuário candidato, podemos criar experimentos em que os usuários em potencial recebem
uma tarefa para concluir e, em seguida, são solicitados a fornecer feedback sobre como encontraram a experiência.
Um formulário simples e manual é mais que adequado para capturar esse feedback:
Minha tarefa foi fácil 4 Concluí a tarefa ok após ser solicitado pelo
de concluir seu pesquisador.
Eu me senti confiante 2 O campo de entrada de texto sobre o tamanho
completando minha tarefa da camiseta me confundiu. Poderia ser uma lista
sem instruções suspensa de opções disponíveis?
A interface me guiou 3 No final das contas tudo bem – mas aquele campo de texto
durante a tarefa foi um aborrecimento, então classifiquei essa tarefa com uma
O design da experiência do usuário é principalmente uma atividade humana. O mesmo ocorre com a avaliação dos resultados dos
testes. Essas ferramentas apenas nos permitem criar uma maquete de nossas visões e coletar resultados experimentais. Devemos
realizar sessões com usuários reais, solicitar suas opiniões sobre como foi sua experiência e depois retroalimentar os resultados
em um design aprimorado.
Embora a experiência do usuário seja importante, a próxima seção trata de um aspecto de missão crítica do nosso código:
segurança e operações.
Machine Translated by Google
Até agora, criamos um aplicativo bem projetado e com defeitos muito baixos. Nosso feedback sobre a experiência do
usuário tem sido positivo – é fácil de usar. Mas todo esse potencial pode ser perdido num instante se não conseguirmos
manter a aplicação em execução. Se os hackers atacarem nosso site e prejudicarem os usuários, a situação ficará ainda
pior.
Um aplicativo que não está em execução não existe. A disciplina de operações – muitas vezes chamada de DevOps
hoje em dia – visa manter os aplicativos funcionando com boa saúde e alertar-nos se essa saúde começar a falhar.
O teste de segurança – também chamado de teste de penetração (pentesting) – é um caso especial de teste
exploratório manual . Por sua natureza, procuramos novas explorações e vulnerabilidades desconhecidas em nossa aplicação.
Esse trabalho não é melhor atendido pela automação. A automação repete o que já é conhecido; descobrir o desconhecido
requer engenhosidade humana.
O teste de penetração é a disciplina que pega um software e tenta contornar sua segurança. As violações de segurança
podem ser caras, embaraçosas ou fatais para uma empresa. As explorações usadas para criar a violação costumam ser
muito simples.
Isto é uma simplificação exagerada, é claro. Mas permanece o facto de que a nossa aplicação pode ser vulnerável a estas
actividades prejudiciais – e precisamos de saber se é esse o caso ou não. Isso requer testes. Esse tipo de teste deve ser
adaptativo, criativo, tortuoso e continuamente atualizado. Uma abordagem automatizada não é nada disso, o que significa
que os testes de segurança devem ocupar o seu lugar como uma etapa manual em nosso processo de desenvolvimento.
Um excelente ponto de partida é revisar os 10 principais riscos de segurança de aplicativos da Web da OWASP mais recentes.
(https://owasp.org/www-project-top-ten/) e inicie alguns testes exploratórios manuais com base
nos riscos listados. Mais informações sobre modelos de ameaças como Spoofing, Adultering,
Repudiation, Information Disclosure, Denial of Service e Elevation of Privilege (STRIDE)
podem ser encontradas em https://www.eccouncil.org/threat-modeling/. OWASP também tem alguns
excelentes recursos sobre ferramentas úteis em https://owasp.org/www-community/Fuzzing.
Fuzzing é uma forma automatizada de descobrir defeitos, embora exija um ser humano para interpretar o
resultados de um teste que falhou.
Tal como acontece com outros testes exploratórios manuais, esses experimentos ad hoc podem levar a alguma automação
de testes futura. Mas o verdadeiro valor reside na criatividade aplicada à investigação do desconhecido.
Machine Translated by Google
As seções anteriores defenderam a importância das intervenções manuais para complementar nossos esforços de
automação de testes. Mas como isso se encaixa em uma abordagem de integração contínua/ entrega contínua (CI/
CD) ? Esse é o foco da próxima seção.
Integrar processos manuais em um pipeline automatizado de CI/CD pode ser difícil. As duas abordagens não são parceiras
naturais em termos de uma sequência linear e repetível de atividades. A abordagem que adotamos depende do nosso
objetivo final. Queremos um sistema de implantação contínua totalmente automatizado ou ficamos satisfeitos com algumas
interrupções manuais?
A abordagem mais simples para incorporar um processo manual é simplesmente parar a automação em um ponto
adequado, iniciar o processo manual e então retomar o autômato quando o processo manual for concluído. Podemos
pensar nisso como um fluxo de trabalho de bloqueio, já que todas as etapas automatizadas do pipeline devem ser
interrompidas até que o trabalho manual seja concluído. Isso é ilustrado no diagrama a seguir:
Ao organizar nosso processo de desenvolvimento como uma série de etapas, algumas automatizadas e outras manuais,
criamos um fluxo de trabalho de bloqueio simples. Bloquear aqui significa que o fluxo de valor é bloqueado em cada
estágio. Os estágios de automação normalmente são executados mais rapidamente que os estágios manuais.
Esse fluxo de trabalho tem algumas vantagens: é simples de entender e operar. Cada iteração de software que entregamos
terá todos os testes automatizados executados, bem como todos os processos manuais atuais. Em certo sentido, este
lançamento é da mais alta qualidade que sabemos fazer naquela época. A desvantagem é que cada iteração deve
aguardar a conclusão de todos os processos manuais:
Um facilitador para fluxos de trabalho de trilha dupla muito suaves é usar um único tronco principal para toda a base de código.
Todos os desenvolvedores se comprometem com este tronco principal. Não há outras filiais. Quaisquer recursos em desenvolvimento
ativo são isolados por sinalizadores de recursos. Esses são valores booleanos que podem ser definidos como verdadeiros ou
falsos em tempo de execução. O código inspeciona esses sinalizadores e decide se um recurso deve ser executado ou não. O
teste manual pode então acontecer sem a necessidade de pausar as implantações. Durante o teste, os recursos em andamento
são habilitados por meio dos sinalizadores de recursos relevantes. Para os usuários finais em geral, os recursos em andamento estão desativados.
Podemos selecionar a abordagem que melhor se adapta aos nossos objetivos de entrega. O fluxo de trabalho de bloqueio
compensa menos retrabalho por um ciclo de entrega estendido. A abordagem dual-track permite entrega de recursos
mais frequente, com risco de defeitos na produção antes que sejam descobertos por um processo manual e,
posteriormente, reparados.
A seleção do processo certo a ser usado envolve uma compensação entre a cadência de lançamento de recursos e a
tolerância a defeitos. Seja qual for a escolha, o objetivo é concentrar a expertise de toda a equipe na criação de software
com baixo índice de defeitos.
Equilibrar fluxos de trabalho automatizados com fluxos de trabalho manuais e humanos não é fácil, mas resulta na
obtenção do máximo de intuição e experiência humana no produto. Isso é bom para nossas equipes de desenvolvimento
e para nossos usuários. Eles se beneficiam de maior facilidade de uso e robustez em suas aplicações.
Esperamos que este capítulo tenha mostrado como podemos combinar esses dois mundos e cruzar a divisão tradicional
entre desenvolvedores e testadores. Podemos formar uma grande equipe, visando um resultado excelente.
Resumo
Este capítulo discutiu a importância de vários processos manuais durante o desenvolvimento.
Apesar de suas vantagens, vimos como o TDD não consegue prevenir todos os tipos de defeitos de software. Primeiro,
abordamos os benefícios de aplicar a criatividade humana em testes exploratórios manuais, onde podemos descobrir
defeitos que não foram percebidos durante o TDD. Em seguida, destacamos as melhorias de qualidade que as revisões
e análises de código trazem. Também abordamos a natureza muito manual da criação e verificação de interfaces de
usuário excelentes com experiências de usuário satisfatórias. Em seguida, enfatizamos a importância dos testes de
segurança e do monitoramento das operações para manter um sistema ativo funcionando bem. Por fim, revisamos
abordagens para integração de etapas manuais em fluxos de trabalho de automação e as compensações que precisamos fazer.
No próximo capítulo, revisaremos algumas formas de trabalhar relacionadas a quando e onde desenvolvemos testes,
antes de passarmos para a Parte 3 deste livro, onde terminaremos de construir nossa aplicação Wordz.
Machine Translated by Google
Perguntas e respostas
A seguir estão algumas perguntas e respostas sobre o conteúdo deste capítulo:
Não. Eles mudaram onde está o valor. Alguns processos manuais tornaram-se irrelevantes, enquanto outros
aumentaram em importância. Tradicionalmente, as etapas manuais, como seguir documentos de teste para
testes de recursos e testes de regressão, não são mais necessárias. A execução de testes de recursos e de
regressão mudou da escrita de planos de teste em um processador de texto para a escrita de código de teste
em um IDE. Mas para muitas tarefas centradas no ser humano, ter uma mente humana envolvida continua a
ser vital para o sucesso.
Isto é desconhecido. Os avanços na IA neste momento (início da década de 2020) provavelmente podem
melhorar a identificação visual e a análise estática de código. É concebível que a análise de imagens da IA
possa um dia ser capaz de fornecer uma análise boa/má da usabilidade – mas isso é pura especulação,
baseada nas capacidades da IA para gerar obras de arte hoje. Tal coisa pode permanecer impossível. Em
termos de conselhos práticos agora, suponha que os processos manuais recomendados neste capítulo
permanecerão manuais por algum tempo.
Leitura adicional
Para saber mais sobre os tópicos abordados neste capítulo, dê uma olhada nos seguintes recursos:
• https://dl.acm.org/doi/pdf/10.1145/274567.274574:
Uma visão geral da gênese moderna do TDD por Kent Beck. Embora as ideias certamente sejam
anteriores a este projeto, esta é a referência central da prática moderna de TDD. Este artigo contém
muitos insights importantes sobre desenvolvimento de software e equipes – incluindo a citação: faça
funcionar, faça certo, faça isso rápido e a necessidade de não sentir que estamos trabalhando o tempo todo. Vale a pena ler.
• https://trunkbaseddevelopment.com/.
• https://martinfowler.com/articles/feature-toggles.html.
• Inspirado: Como criar produtos tecnológicos que os clientes adoram, Marty Cagan, ISBN 978-1119387503:
Um livro interessante que fala sobre gerenciamento de produtos. Embora isso possa parecer estranho em um
livro para desenvolvedores sobre TDD, muitas das ideias neste capítulo vieram da experiência do
desenvolvedor em um projeto ágil de via dupla, seguindo este livro. Dual ágil significa que o feedback rápido
se transforma na descoberta de recursos e se transforma em entrega rápida de feedback ágil/TDD.
Essencialmente, o TDD manual é feito no nível dos requisitos do produto. Este livro é uma leitura interessante
sobre o gerenciamento moderno de produtos, que adotou os princípios do TDD para validação rápida de
suposições sobre recursos do usuário. Muitas ideias neste capítulo visam melhorar o software no nível do produto.
Machine Translated by Google
Machine Translated by Google
12
Teste primeiro, teste depois, teste nunca
Neste capítulo, revisaremos algumas das nuances do Desenvolvimento Orientado a Testes (TDD).
Já cobrimos as técnicas amplas de escrita de testes unitários como parte de uma estratégia geral de teste. Podemos
usar a pirâmide de testes e a arquitetura hexagonal para orientar o escopo de nossos testes em termos do que
especificamente eles precisam cobrir.
Temos mais duas dimensões que precisamos decidir: quando e onde começar os testes. A primeira questão é de timing. Devemos
sempre escrever nossos testes antes do código? Que diferença faria escrever testes após o código? Na verdade, que tal não testar
nada – isso faz sentido?
Onde começar os testes é outra variável a ser decidida. Existem duas escolas de pensamento quando se trata de TDD – testar de
dentro para fora ou de fora para dentro. Analisaremos o que esses termos significam e qual o impacto que cada um tem em nosso
trabalho. Finalmente, consideraremos como essas abordagens funcionam com uma arquitetura hexagonal para formar um limite
natural de teste.
• Testes? Eles são para pessoas que não sabem escrever código!
Nesta seção, revisaremos as vantagens de adicionar um teste antes de escrever o código de produção para fazê-lo passar.
Os capítulos anteriores seguiram uma abordagem de teste inicial para escrever código. Escrevemos um teste antes de escrever o
código de produção para que esse teste seja aprovado. Esta é uma abordagem recomendada, mas é importante compreender
algumas das dificuldades associadas a ela, bem como considerar os seus benefícios.
Machine Translated by Google
O benefício mais importante de escrever testes primeiro é que o teste atua como um auxílio ao design. À medida que decidimos
o que escrever em nosso teste, estamos projetando a interface do nosso código. Cada um dos estágios de teste nos ajuda a
considerar um aspecto do design de software, conforme ilustrado pelo diagrama a seguir:
A etapa Organizar nos ajuda a pensar sobre como o código em teste se relaciona com o panorama geral de toda
a base de código. Esta etapa nos ajuda a projetar como o código se encaixará em toda a base de código. Isso
nos dá a oportunidade de tomar as seguintes decisões de design:
Codificar a etapa Act nos permite pensar em quão fácil será usar nosso código. Refletimos sobre como gostaríamos que fosse a
assinatura do método do código que estamos projetando. Idealmente, deve ser simples e inequívoco. Algumas recomendações gerais
são as seguintes:
• Passe o menor número possível de parâmetros. Possivelmente agrupe parâmetros em seu próprio objeto.
• Evite sinalizadores booleanos que modificam o comportamento do código. Use métodos separados com
nomes apropriados.
• Evite exigir múltiplas chamadas de método para fazer uma coisa. É muito fácil perder algo importante
chame na sequência se não estivermos familiarizados com o código.
Machine Translated by Google
Escrever a etapa Act nos permite ver como será a chamada ao nosso código em todos os lugares em que for usado pela primeira
vez. Isso oferece a oportunidade de simplificar e esclarecer antes que nosso código seja amplamente utilizado.
O código em nossa etapa Assert é o primeiro consumidor dos resultados do nosso código. Podemos julgar a partir desta etapa
se esses resultados são fáceis de obter. Se não estivermos satisfeitos com a aparência do código Assert, esta é uma oportunidade
de revisar como nosso objeto fornece sua saída.
Cada teste que escrevemos oferece esta oportunidade para uma revisão do projeto. O objetivo do TDD é nos ajudar a descobrir
projetos melhores, ainda mais do que testar a correção.
Em outras indústrias, como a de design de carros, é comum ter ferramentas de design dedicadas. O AutoCAD
3D Studio é usado para criar modelos 3D do chassi de um carro em um computador. Antes de fabricarmos o
carro, podemos utilizar a ferramenta para pré-visualizar o resultado final, girando-o pelo espaço e visualizando-
o de vários ângulos.
A engenharia de software comercial convencional está muito atrás em termos de suporte a ferramentas de design. Não
temos equivalente ao 3D Studio para projetar código. As décadas de 1980 a 2000 viram o surgimento das ferramentas
de Engenharia de Software Auxiliada por Computador (CASE) , mas estas parecem ter caído em desuso. As
ferramentas CASE pretendiam simplificar a engenharia de software, permitindo que seus usuários inserissem várias
formas gráficas de estruturas de software e, em seguida, gerassem código que implementasse essas estruturas. Hoje,
escrever testes TDD antes de escrever o código de produção parece ser a coisa mais próxima que temos do design de
software auxiliado por computador atualmente.
Documentação adicional é útil. Documentos como logs RAID – que documentam riscos, ações, problemas e
decisões – e KDDs – que documentam as principais decisões de projeto – são frequentemente necessários.
Estes são documentos não executáveis. Eles servem ao propósito de capturar quem, quando e por que uma
decisão importante foi tomada. Informações desse tipo não podem ser capturadas por meio de código de teste,
o que significa que esses tipos de documentos têm valor.
Uma ferramenta de cobertura de código instrumenta nosso código de produção à medida que executamos os testes.
Esta instrumentação captura quais linhas de código foram executadas durante a execução dos testes. Este relatório pode
sugerir que faltam testes, sinalizando linhas de código que nunca foram executadas durante a execução do teste.
O relatório de cobertura de código na imagem mostra que executamos 100% do código no modelo de domínio em nosso
teste. Ter 100% de cobertura depende inteiramente de escrevermos um teste TDD antes de escrevermos o código para
fazê-lo passar. Não adicionamos código não testado com um fluxo de trabalho TDD de teste inicial.
Uma métrica de alta cobertura de código nem sempre indica alta qualidade de código. Se estivermos escrevendo
testes para código gerado ou testes para código extraído de uma biblioteca, essa cobertura não nos diz nada
de novo. Podemos assumir – geralmente – que nossos geradores de código e bibliotecas já foram testados por
seus desenvolvedores.
No entanto, um problema real com os números de cobertura de código acontece quando os determinamos como uma métrica.
Assim que impusermos uma meta de cobertura mínima aos promotores, então a lei de Goodhart se aplica –
quando uma medida se torna uma meta, deixa de ser uma boa medida. Às vezes, os humanos enganam o sistema
para atingir uma meta quando estão sob pressão. Quando isso acontecer, você verá um código como este:
// assertThat(pontuação).isEqualTo(CORRETO);
Observe aqueles símbolos de comentário – // – logo antes de assertThat()? Essa é a marca registrada de um
caso de teste que estava falhando e não pôde ser aprovado dentro de um determinado prazo. Ao manter o
teste, mantemos alto nosso número de casos de teste e nossa porcentagem de cobertura de código. Um teste
como esse executará linhas de código de produção, mas não validará se elas funcionam. A meta de cobertura
de código será atingida – mesmo que o código em si não funcione.
Agora, eu sei o que você está pensando – nenhum desenvolvedor jamais trapacearia o código de teste dessa maneira. É, no
entanto, um exemplo de um projeto em que trabalhei para um grande cliente internacional. O cliente contratou a empresa em
que trabalho e outra equipe de desenvolvimento para trabalhar em alguns microsserviços. Devido a uma diferença de fuso
horário, a outra equipe verificaria as alterações de código enquanto nossa equipe estivesse dormindo.
Certa manhã, chegamos e vimos nossos painéis de resultados de testes iluminados em vermelho. A mudança de código
durante a noite fez com que um grande número de nossos testes falhasse. Verificamos os pipelines da outra equipe e ficamos
surpresos ao ver todos os seus testes sendo aprovados. Isso não fazia sentido. Nossos testes revelaram claramente um
defeito na queda noturna do código. Poderíamos até localizá-lo a partir de nossas falhas nos testes. Esse defeito teria
aparecido nos testes de unidade em torno desse código, mas esses testes de unidade foram aprovados. A razão? Afirmações comentadas.
A outra equipe estava sob pressão para entregar. Eles obedeceram às instruções para verificar a alteração do código naquele
dia. Essas mudanças, na verdade, quebraram seus testes unitários. Quando não conseguiram resolver os problemas no
tempo disponível, optaram por enganar o sistema e adiar o problema para outro dia. Não tenho certeza se os culpo. Às vezes,
100% de cobertura de código e aprovação em todos os testes não significam absolutamente nada.
Um dos pontos fortes do TDD é que ele permite design emergente. Fazemos um pequeno trabalho de design, capturado em
um teste. Em seguida, fazemos a próxima pequena peça de design, capturada em um novo teste. Realizamos diferentes
profundidades de refatoração à medida que avançamos. Dessa forma, aprendemos sobre o que está ou não funcionando em
nossa abordagem. Os testes fornecem feedback rápido sobre nosso design.
Isso só pode acontecer se escrevermos um teste de cada vez. Uma tentação para aqueles familiarizados com abordagens de
projetos em cascata pode ser tratar o código de teste como um documento gigante de requisitos, a ser concluído antes do
início do desenvolvimento. Embora isso pareça mais promissor do que simplesmente escrever um documento de requisitos
em um processador de texto, também significa que os desenvolvedores não podem aprender com o feedback do teste. Não
há ciclo de feedback. Esta abordagem de teste deve ser evitada. Melhores resultados são obtidos adotando uma abordagem
incremental. Escrevemos um teste de cada vez, junto com o código de produção para fazer o teste passar.
Machine Translated by Google
A única razão para o código não ser implantado neste sistema – assumindo que o código é compilado – é se os testes
falharem. Isto implica que os testes automatizados que implementamos são necessários e suficientes para criar o nível de
confiança necessário.
Escrever testes primeiro não pode garantir isso – ainda podemos ter testes faltando – mas de todas as maneiras de trabalhar
com testes, talvez seja a mais provável que resulte em um teste significativo para cada parte do comportamento do aplicativo
que nos interessa.
Esta seção apresentou o caso de que escrever testes primeiro – antes de o código de produção ser escrito, para fazê- los
passar – ajuda a criar confiança em nosso código, bem como em especificações executáveis úteis. No entanto, essa não é
a única maneira de codificar. Na verdade, uma abordagem comum que veremos é escrever primeiro um pedaço de código
e depois escrever testes logo depois.
Uma abordagem para escrever testes envolve escrever pedaços de código e, em seguida, adaptar os testes a esses
pedaços de código. É uma abordagem usada em programação comercial, e o fluxo de trabalho pode ser ilustrado da
seguinte forma:
Ao selecionar uma história de usuário para desenvolver, uma ou mais partes do código de produção são escritas. Seguem testes!
A pesquisa acadêmica parece confusa, para dizer o mínimo, sobre se o teste depois difere ou não do teste primeiro.
De um estudo de 2014 da ACM, um extrato da conclusão foi este:
(Fonte: https://dl.acm.org/doi/10.1145/2601248.2601267)
Machine Translated by Google
“…os dados utilizáveis foram obtidos de apenas 13 dos 31 desenvolvedores. Isto significa que a
análise estatística foi realizada utilizando grupos de sete (TDD) e seis (TLD).
Não há nenhuma surpresa real que o experimento tenha faltado poder estatístico e que as descobertas
tenham sido inconclusivas.”
Outros trabalhos de pesquisa parecem mostrar resultados semelhantes e sem brilho. Na prática, então, o que devemos tirar disso?
Vamos considerar alguns detalhes práticos do desenvolvimento de teste posterior.
Uma descoberta da pesquisa foi que os iniciantes em TDD descobriram que o teste mais tarde é mais fácil de começar . Isto parece
razoável. Antes de tentarmos o TDD, podemos considerar a codificação e o teste como atividades diferentes. Escrevemos código de
acordo com algum conjunto de heurísticas e então descobrimos como testar esse código. A adoção de uma abordagem de teste
posterior significa que a fase de codificação permanece essencialmente inalterada pelas demandas de teste. Podemos continuar
codificando como sempre fizemos. Não há impacto em ter que considerar os impactos dos testes no design desse código. Essa
aparente vantagem dura pouco, pois descobrimos a necessidade de adicionar pontos de acesso para testes, mas pelo menos
podemos começar facilmente.
Adicionar testes posteriormente funciona razoavelmente bem se continuarmos escrevendo testes em sincronia com o código de
produção: escreva um pequeno código e escreva alguns testes para esse código – mas não ter testes para cada caminho de código
continua sendo um risco.
Testar mais tarde torna mais difícil testar todos os caminhos de código
Um argumento plausível contra o uso de uma abordagem de teste posterior é que fica mais difícil controlar todos os testes de que
precisamos. À primeira vista, esta afirmação não pode ser completamente verdadeira. Sempre podemos encontrar uma maneira de
acompanhar os testes de que precisamos. Um teste é um teste, não importa quando é escrito.
O problema surge à medida que o tempo entre a adição de testes aumenta. Estamos adicionando mais código, o que
significa adicionar mais caminhos de execução ao longo do código. Por exemplo, cada instrução if que escrevemos
representa dois caminhos de execução. Idealmente, todo caminho de execução em nosso código terá um teste. Cada
caminho de execução não testado que adicionamos nos coloca um teste abaixo desse número ideal. Isso é ilustrado
diretamente nos fluxogramas:
Machine Translated by Google
Este fluxograma representa um processo com pontos de decisão aninhados – os formatos de losango – que
resultam em três caminhos de execução possíveis, rotulados como A, B e C. A medida técnica do número de
caminhos de execução é chamada de complexidade ciclomática. A pontuação de complexidade é o número
calculado sobre quantos caminhos de execução linearmente independentes existem em um trecho de código. O
código no fluxograma tem uma complexidade ciclomática de três.
À medida que aumentamos a complexidade ciclomática do nosso código, aumentamos a nossa carga cognitiva com a
necessidade de lembrar de todos aqueles testes que precisaremos escrever mais tarde. Em algum momento, podemos até
parar periodicamente de codificar e fazer anotações sobre quais testes adicionar mais tarde. Parece uma versão mais
árdua de simplesmente escrever os testes à medida que avançamos.
A questão de acompanhar os testes que ainda não escrevemos é evitada ao usar o desenvolvimento test-first.
Ao escrever testes depois que um pedaço de código já foi escrito, fica mais difícil incorporar feedback. Podemos descobrir
que o código que criamos é difícil de integrar ao restante da base de código. Talvez este código seja confuso de usar
devido a interfaces pouco claras. Dado todo o esforço que despendemos para criar o código confuso, pode ser tentador
simplesmente conviver com o design estranho e seu código de teste igualmente estranho.
Machine Translated by Google
Testes? Eles são para pessoas que não sabem escrever código! 221
O desenvolvimento tende a ser uma atividade movimentada, especialmente quando há prazos envolvidos. As pressões de tempo
podem significar que o tempo que esperávamos chegar para escrever nossos testes simplesmente nunca chega. Não é incomum que
os gerentes de projeto fiquem mais impressionados com os novos recursos do que com os testes. Isto parece uma falsa economia –
já que os usuários só se preocupam com recursos que funcionam – mas é uma pressão que os desenvolvedores às vezes enfrentam.
Esta seção mostrou que escrever testes logo após escrever o código pode funcionar tão bem quanto escrever testes primeiro,
se houver cuidado. Também parece preferível para alguns desenvolvedores no início de sua jornada TDD – mas e quanto ao
extremo de nunca testar nosso código? Vamos revisar rapidamente as consequências dessa abordagem.
Testes? Eles são para pessoas que não sabem escrever código!
Esta seção discute outra possibilidade óbvia quando se trata de testes automatizados – simplesmente não escrever testes
automatizados. Talvez nem mesmo testando. Isso é viável?
Não testar é uma escolha que poderíamos fazer, e isso pode não ser tão bobo quanto parece. Se definirmos o teste como a
verificação de que algum resultado é alcançado no ambiente alvo, então coisas como sondas do espaço profundo não podem
ser verdadeiramente testadas na Terra. Na melhor das hipóteses, estamos simulando o ambiente de destino durante nossos
testes. Aplicações web em escala gigante raramente podem ser testadas com perfis de carga realistas. Pegue qualquer
aplicativo web grande, lance nele cem milhões de usuários – todos fazendo coisas inválidas – e veja como a maioria dos
aplicativos se comporta. Provavelmente não é tão bom quanto sugerido pelos testes do desenvolvedor.
Existem áreas de desenvolvimento onde podemos esperar ver menos testes automatizados:
Os scripts ETL geralmente são casos únicos, escritos para resolver um problema específico de migração com alguns
dados. Nem sempre vale a pena escrever testes automatizados para eles, em vez disso, realizar a verificação manual
em um conjunto semelhante de dados de origem.
Dependendo da abordagem de programação, pode ser um desafio escrever testes unitários para o código frontend.
Qualquer que seja a abordagem adotada, a avaliação da aparência visual não pode atualmente ser automatizada.
Como resultado, o teste manual é frequentemente usado em uma versão candidata de uma interface de usuário.
Nossos aplicativos precisam ser implantados em algum lugar para serem executados. Uma abordagem recente para
implantação é usar linguagens como Terraform para configurar servidores usando código. Esta é uma área para a qual
ainda não é simples automatizar testes.
Então, o que realmente acontece quando abandonamos a automação de testes, possivelmente nem mesmo testando?
Machine Translated by Google
As abordagens de teste primeiro mudam os testes para o mais cedo possível – uma abordagem chamada shift-left – onde
os defeitos podem ser corrigidos de maneira fácil e barata. Pensar que não testaremos apenas empurra os testes para a
direita – depois que os usuários começarem a usar os recursos ao vivo.
Em última análise, todo o código que interessa aos usuários é testado eventualmente. Talvez os desenvolvedores não testem.
Talvez os testes caiam para outra equipe de testes especializada, que escreverá relatórios de defeitos. Talvez sejam
encontrados defeitos durante a operação do software. O mais comum é que acabamos terceirizando os testes para os
próprios usuários.
Fazer com que os usuários testem nosso código geralmente é uma má ideia. Os usuários confiam em nós para fornecer
software que resolva seus problemas. Sempre que um defeito em nosso código impede que isso aconteça, perdemos essa
confiança. A perda de confiança prejudica os 3 Rs de uma empresa: receita, reputação e retenção. Os usuários podem muito
bem mudar para outro fornecedor, cujo código mais bem testado realmente resolva o problema do usuário.
Se houver alguma possibilidade de testar nosso trabalho antes de enviá-lo, devemos aproveitar essa oportunidade. Quanto
mais cedo incorporarmos ciclos de feedback baseados em testes em nosso trabalho, mais fácil será melhorar a qualidade
desse trabalho.
Tendo observado quando testamos nosso software, vamos ver onde o testamos. Dado o design geral de um software, por
onde devemos começar os testes? A próxima seção analisa uma abordagem de teste que começa no interior de um projeto
e vai até o fim.
Ao começar a construir software, obviamente precisamos de algum ponto de partida. Um lugar para começar
é com alguns detalhes. O software é composto de pequenos componentes interconectados, cada um dos quais
Machine Translated by Google
executa uma parte de toda a tarefa. Alguns componentes vêm do código da biblioteca. Muitos componentes são
feitos sob medida para fornecer a funcionalidade que nosso aplicativo precisa.
Um lugar para começar a construir é dentro deste sistema de software. Começando com uma história de usuário geral, podemos imaginar um pequeno
componente que provavelmente será útil para nós. Podemos começar nossos esforços de TDD em torno deste componente e ver aonde isso nos leva. Esta é
uma abordagem de baixo para cima do design, compondo o todo a partir de partes menores.
Se considerarmos uma versão simplificada da estrutura do nosso aplicativo Wordz, podemos ilustrar a abordagem de dentro para fora da seguinte forma:
O diagrama mostra o componente Score em destaque, pois é aí que iniciaremos o desenvolvimento usando uma abordagem de dentro para fora. Os outros
componentes de software estão esmaecidos. Ainda não estamos projetando essas peças. Começaríamos com um teste para algum comportamento que
queríamos que o componente Score tivesse. Trabalharíamos nosso caminho a partir desse ponto de partida.
Este estilo de TDD de dentro para fora também é conhecido como Classicist TDD ou Chicago TDD. É a abordagem
originalmente descrita por Kent Beck em seu livro Test-Driven Development by Example. A ideia básica é começar de
qualquer lugar para criar qualquer bloco de construção útil para nosso código. Em seguida, desenvolvemos uma unidade
progressivamente maior que incorpora os blocos de construção anteriores.
• Início rápido do desenvolvimento: testamos primeiro o código Java puro nesta abordagem, usando as ferramentas familiares de JUnit e AssertJ. Não
Não há configuração de ferramentas de teste de interface do usuário. Nós simplesmente mergulhamos de cabeça e codificamos usando Java.
Machine Translated by Google
• Bom para projetos conhecidos: À medida que ganhamos experiência, reconhecemos alguns problemas como
tendo soluções conhecidas. Talvez já tenhamos escrito algo semelhante antes. Talvez conheçamos uma coleção
útil de padrões de projeto que funcionarão. Nestes casos, faz sentido partir da estrutura interna do nosso código.
• Funciona bem com arquitetura hexagonal: O TDD de dentro para fora começa a trabalhar dentro do hexágono
interno, o modelo de domínio de nossa aplicação. A camada adaptadora forma um limite natural. Um design de
dentro para fora é uma boa opção para essa abordagem de design.
Naturalmente, nada é perfeito e o TDD de dentro para fora não é exceção. Alguns desafios incluem o seguinte:
• Possibilidade de desperdício: Começamos o TDD de dentro para fora com nossa melhor estimativa de alguns
componentes que serão necessários. Às vezes, mais tarde descobrimos que não precisamos desses componentes
ou que deveríamos refatorar os recursos em outro lugar. O nosso esforço inicial é, em certo sentido, desperdiçado
– embora nos tenha ajudado a progredir até este ponto.
O TDD de dentro para fora é uma abordagem útil e foi popularizado pela primeira vez no livro de Kent Beck. Porém, se
pudermos começar de dentro para fora, que tal reverter isso? E se começássemos de fora do sistema e ingressássemos?
A próxima seção analisa essa abordagem alternativa.
O TDD externo começa com os usuários externos do sistema. Podem ser usuários humanos ou máquinas, consumindo
alguma API oferecida pelo nosso software. Esta abordagem ao TDD começa simulando alguma entrada externa, como o
envio de um formulário web.
O teste normalmente usará algum tipo de estrutura de teste – como Selenium ou Cypress para aplicativos da web – que
permite ao teste acessar uma visualização da web específica e simular a digitação de texto nos campos e, em seguida,
clicar em um botão de envio. Podemos então fazer esse teste passar da maneira normal, só que desta vez teremos escrito
algum código que lide diretamente com a entrada de um usuário. Em nosso modelo de arquitetura hexagonal, acabaremos
escrevendo primeiro o adaptador de entrada do usuário.
Machine Translated by Google
Podemos ver que um componente chamado Web API é o foco de nossa atenção aqui. Escreveremos um teste que
configure nosso aplicativo o suficiente para executar um componente que lida com solicitações da web. O teste irá formar
uma solicitação da web, enviá-la ao nosso software e, em seguida, afirmar que a resposta da web correta foi enviada. O
teste também pode instrumentar o próprio software para verificar se ele executa as ações esperadas internamente.
Começamos os testes de fora e, à medida que o desenvolvimento avança, avançamos para dentro.
Essa abordagem para TDD é descrita no livro Growing Object-Oriented Software, Guided by Tests, de Steve
Freeman e Nat Pryce. A técnica também é conhecida como escola de Londres ou Mockist de TDD. As
razões para isso são o local onde foi popularizado pela primeira vez e o uso de objetos simulados,
respectivamente. Para testar o adaptador de entrada do usuário como o primeiro componente que
abordamos, precisamos de um teste duplo no lugar do restante do software. Mocks e stubs são uma parte inerente do TDD externo
O TDD externo, previsivelmente, tem alguns pontos fortes e fracos. Vamos dar uma olhada nos pontos fortes primeiro:
• Menos desperdício: o TDD de fora para dentro incentiva uma abordagem mínima para satisfazer o comportamento externo.
O código produzido tende a ser altamente customizado para a aplicação em questão. Em contraste, o TDD de dentro para
fora concentra-se na construção de um modelo de domínio robusto, talvez fornecendo mais funcionalidades do que as que
serão utilizadas pelos usuários.
• Fornece valor ao usuário rapidamente: como partimos de um teste que simula uma solicitação do usuário, o código que
escrevemos irá satisfazer a solicitação do usuário. Podemos agregar valor aos usuários quase imediatamente.
Machine Translated by Google
O TDD externo também tem alguns pontos fracos, ou pelo menos limitações:
• Menos abstrações: Por outro lado, ao escrever o código mínimo necessário para passar no teste, o TDD
de fora para dentro pode fazer com que a lógica do aplicativo esteja presente na camada do adaptador.
Isso pode ser refatorado posteriormente, mas pode levar a uma base de código menos organizada.
• Pirâmide de teste invertida: Se todos os nossos esforços de teste TDD se concentrarem nas respostas externas, eles serão, na
verdade, testes ponta a ponta. Isso se opõe ao padrão recomendado da pirâmide de testes, que prefere testes unitários mais rápidos
dentro da base de código. Ter apenas testes ponta a ponta mais lentos e menos repetíveis pode retardar o desenvolvimento.
As duas escolas tradicionais de TDD oferecem certas vantagens em termos de como afetam o design de software que iremos produzir. A
próxima seção analisa o impacto da arquitetura hexagonal. Partindo da ideia de que utilizaremos uma abordagem hexagonal, podemos
combinar as vantagens de ambas as escolas de TDD. Acabamos definindo um limite de teste natural entre as abordagens de dentro para fora
De certa forma, a forma como organizamos nossa base de código não afeta nosso uso do TDD. A estrutura interna do código é simplesmente
um detalhe de implementação, uma das muitas possibilidades que farão nossos testes passarem. Dito isto, algumas formas de estruturar
nosso código são mais fáceis de trabalhar do que outras. Usar a arquitetura hexagonal como estrutura fundamental oferece algumas
Aprendemos nos capítulos anteriores que é mais fácil escrever testes para código onde podemos
controlar o ambiente no qual o código é executado. Vimos como a pirâmide de teste dá uma estrutura ao
diferentes tipos de testes que escrevemos. O uso da abordagem de portas e adaptadores fornece limites claros para cada tipo de teste no
código. Melhor ainda, nos dá a oportunidade de trazer ainda mais testes para o nível de teste unitário.
Vamos revisar quais tipos de testes se adaptam melhor a cada camada de software escrito usando arquitetura hexagonal.
O TDD clássico usa uma abordagem de desenvolvimento de dentro para fora, onde escolhemos um determinado componente de software
para testar. Este componente pode ser uma única função, uma única classe ou um pequeno cluster de classes que colaboram entre si.
Utilizamos o TDD para testar esse componente como um todo, dados os comportamentos que ele oferece aos seus consumidores.
Machine Translated by Google
A principal vantagem é que é fácil escrever testes para esses componentes e eles são executados muito rapidamente.
Tudo vive na memória do computador e não há sistemas externos com os quais lidar.
Uma vantagem adicional é que comportamentos complexos podem ser testados em unidade aqui com uma granularidade muito fina. Um
exemplo seria testar todas as transições de estado dentro de uma máquina de estados finitos usada para controlar um fluxo de trabalho.
Uma desvantagem é que esses testes lógicos de domínio refinados podem ser perdidos se ocorrer uma refatoração
maior . Se o componente sob testes refinados for removido durante a refatoração, seu teste correspondente será perdido
– mas o comportamento ainda existirá em outro lugar como resultado dessa refatoração. Uma coisa que as ferramentas
de refatoração não podem fazer é descobrir qual código de teste está relacionado ao código de produção que está sendo
refatorado e refatorar automaticamente o código de teste para se adequar à nova estrutura.
Esses testes de integração precisam apenas cobrir o comportamento fornecido pelo adaptador. Isto deverá ter um alcance muito
limitado. O código do adaptador mapeia os formatos usados pelo sistema externo apenas para o que é exigido pelo modelo de
domínio. Não tem outra função.
Esta estrutura segue naturalmente as diretrizes da pirâmide de testes. São necessários menos testes de integração. Cada teste de
integração possui apenas um pequeno escopo de comportamento para testar:
Esse estilo de teste verifica o adaptador isoladamente. Serão necessários alguns testes de caminho feliz de ponta a ponta para
mostrar que o sistema como um todo usou os adaptadores corretos.
Um benefício de ter um modelo de domínio contendo toda a lógica do aplicativo é que podemos testar a lógica de histórias de
usuários completas. Podemos substituir os adaptadores por testes duplos para simular respostas típicas dos sistemas externos.
Podemos então usar os FIRST testes de unidade para exercitar histórias de usuários completas:
Machine Translated by Google
As vantagens são a velocidade e a repetibilidade dos testes unitários do FIRST. Em outras abordagens para estruturar nosso
código, talvez só possamos exercitar uma história de usuário como um teste ponta a ponta em um ambiente de teste, com
todas as desvantagens associadas. Ter a capacidade de testar a lógica da história do usuário no nível da unidade – em todo
o modelo de domínio – nos dá um alto grau de confiança de que nosso aplicativo irá satisfazer as necessidades dos usuários.
Para garantir essa confiança, precisamos de testes de integração da camada do adaptador, além de alguns testes ponta a
ponta em histórias de usuários selecionadas, confirmando que o aplicativo está conectado e configurado corretamente como
um todo. Esses testes de nível superior não precisam ser tão detalhados quanto os testes de história de usuário realizados em
torno do modelo de domínio.
Ter um bom conjunto de testes de histórias de usuários em torno do modelo de domínio também permite a refatoração em
larga escala dentro do modelo de domínio. Podemos ter confiança para reestruturar o hexágono interno guiados por esses
testes de história de usuário de amplo escopo.
Esta seção nos mostrou como relacionar os diferentes tipos de testes na pirâmide de testes com as diferentes camadas de
uma arquitetura hexagonal.
Machine Translated by Google
Resumo
Este capítulo discutiu os vários estágios em que podemos escrever testes – antes de escrevermos código, depois de
escrevermos código, ou possivelmente até nunca. Ele defendeu a escrita de testes antes do código como fornecendo o
maior valor em termos de cobertura válida do caminho de execução e facilidade para o desenvolvedor. Passamos a revisar
como a arquitetura hexagonal interage com o TDD e a pirâmide de testes, levando a uma oportunidade de trazer os testes
de histórias de usuários para o domínio dos testes de unidade FIRST. Isso permite a validação rápida e repetível da lógica
central que orienta nossas histórias de usuários.
No próximo capítulo – e ao longo da terceira parte do livro – voltaremos à construção de nossa aplicação Wordz. Faremos
pleno uso de todas as técnicas que aprendemos até agora. Começaremos de dentro para fora com o Capítulo 13,
Conduzindo a Camada de Domínio.
Perguntas e respostas
1. Escrever testes logo após o código é tão bom quanto escrever TDD de teste primeiro?
Algumas pesquisas parecem sugerir isso, embora seja muito difícil montar um experimento controlado com
resultados estatisticamente significativos nesta área. Um fator que podemos considerar diz respeito à nossa
própria disciplina pessoal. Se escrevermos testes mais tarde, temos certeza de que cobriremos tudo o que for
necessário? Pessoalmente, concluí que não me lembraria de tudo o que precisava cobrir e precisaria fazer
anotações. Essas notas talvez sejam melhor capturadas na forma de código de teste, levando a uma preferência
pelo TDD de teste primeiro.
A arquitetura hexagonal fornece uma separação clara entre um núcleo interno puro de lógica de domínio e o
mundo externo. Isso nos permite misturar e combinar as duas escolas de TDD sabendo que existe um limite firme
no design até o qual podemos codificar. O modelo de domínio interno suporta casos de uso inteiros sendo testados
em unidade, bem como quaisquer testes de unidade refinados para comportamento detalhado que consideramos
necessários. Os adaptadores externos são naturalmente adequados para testes de integração, mas esses testes
não precisam cobrir muito, pois a lógica está relacionada ao nosso domínio que reside no modelo de domínio interno.
Exportamos a responsabilidade para o usuário final que irá testá-la para nós. Corremos o risco de perda de
receita, reputação e retenção de usuários. Às vezes, não conseguimos recriar perfeitamente o ambiente final em
que o sistema será utilizado. Nesse caso, parece sensato certificar-se de que caracterizamos e testamos
totalmente nosso sistema o mais fielmente possível. Podemos pelo menos minimizar os riscos conhecidos.
Machine Translated by Google
Leitura adicional
• Uma explicação da métrica Complexidade Ciclomática: https://en.wikipedia.org/
wiki/Complexidade Ciclomática
• Entrega Contínua, Jez Humble e Dave Farley, ISBN 978-0321601919
• Desenvolvimento de software orientado a objetos, guiado por testes, Steve Freeman e Nat Pryce,
ISBN 9780321503626
• https://arxiv.org/pdf/1611.05994.pdf
• Por que a pesquisa sobre desenvolvimento orientado a testes é inconclusiva?, Ghafari, Gucci, Gross e
Felderer: https://arxiv.org/pdf/2007.09863.pdf
Machine Translated by Google
Machine Translated by Google
Parte 3:
TDD do mundo real
A Parte 3 é onde aplicamos todas as técnicas que aprendemos para completar nossa aplicação. Wordz é um serviço web
que joga um jogo de adivinhação de palavras. Baseamos-nos na lógica de domínio central que já construímos, adicionando
armazenamento através de um banco de dados Postgres acessado usando SQL e fornecendo acesso à web implementando
uma API REST HTTP.
Usaremos testes de integração para testar nosso banco de dados e implementações de API, fazendo uso de estruturas de teste que
simplificam essas tarefas. No capítulo final do livro, reuniremos tudo para executar com segurança nosso aplicativo Wordz orientado a
testes.
13
Conduzindo a camada de domínio
Estabelecemos muitas bases nos capítulos anteriores, cobrindo uma mistura de técnicas de TDD e abordagens
de design de software. Agora podemos aplicar esses recursos para construir nosso jogo Wordz. Construiremos
com base no código útil que escrevemos ao longo do livro e trabalharemos em direção a um design bem
projetado e testado, escrito usando a abordagem de teste primeiro.
Nosso objetivo neste capítulo é criar a camada de domínio do nosso sistema. Adotaremos a abordagem de
arquitetura hexagonal conforme descrito no Capítulo 9, Arquitetura Hexagonal – Desacoplamento de Sistemas Externos.
O modelo de domínio conterá toda a lógica principal do nosso aplicativo. Este código não estará vinculado a detalhes
de quaisquer tecnologias de sistema externo, como bancos de dados SQL ou servidores web. Criaremos abstrações
para esses sistemas externos e usaremos testes duplos para nos permitir testar a lógica do aplicativo.
Usar a arquitetura hexagonal dessa forma nos permite escrever os PRIMEIROS testes de unidade para histórias de
usuários completas, o que geralmente requer integração ou testes ponta a ponta em outras abordagens de design.
Escreveremos nosso código de modelo de domínio aplicando as ideias apresentadas no livro até agora.
• Jogando o jogo
• Terminar o jogo
Requerimentos técnicos
O código final deste capítulo pode ser encontrado em https://github.com/PacktPublishing/
Desenvolvimento orientado a testes com Java/tree/main/chapter13.
Diante disso, podemos começar bem considerando o que precisa acontecer quando iniciamos um novo jogo.
Isso deve deixar as coisas prontas para jogar e, portanto, forçará a tomada de algumas decisões críticas.
• Como jogador, quero começar um novo jogo para ter uma nova palavra para adivinhar
2. Armazene a palavra selecionada para que as pontuações das suposições possam ser calculadas
Assumiremos o uso de arquitetura hexagonal ao codificarmos esta história, o que significa que qualquer sistema externo será
representado por uma porta no modelo de domínio. Com isso em mente, podemos criar nosso primeiro teste e partir daí.
É importante ressaltar que podemos escrever testes unitários que cubram toda a lógica necessária para uma história de usuário.
Se escrevêssemos um código vinculado a sistemas externos – por exemplo, que contivesse instruções SQL e estivesse
conectado a um banco de dados – precisaríamos de um teste de integração para cobrir uma história de usuário. Nossa escolha
pela arquitetura hexagonal nos liberta disso.
Em uma nota tática, reutilizaremos classes que já testamos, como classe WordSelection,
classe Word e classe Score. Reutilizaremos códigos existentes e bibliotecas de
terceiros sempre que surgir uma oportunidade.
Nosso ponto de partida é escrever um teste para capturar nossas decisões de design relacionadas ao início de um novo jogo:
1. Começaremos com um teste chamado NewGameTest. Este teste atuará em todo o modelo de domínio para
exclua nosso manejo de tudo o que precisamos fazer para iniciar um novo jogo:
pacote com.wordz.domain;
}
Machine Translated by Google
2. Para este teste, começaremos primeiro com a etapa Agir. Estamos assumindo uma arquitetura hexagonal,
então o objetivo de design da etapa Act é projetar a porta que trata da solicitação para iniciar um novo
jogo. Na arquitetura hexagonal, uma porta é o trecho de código que permite que algum sistema externo se
conecte ao modelo de domínio. Começamos criando uma classe para nosso port:
pacote com.wordz.domain;
A principal decisão de design aqui é criar uma classe de controlador para lidar com a solicitação de
início de um jogo. É um controlador no sentido do livro original Gang of Four's Design Patterns – um
objeto de modelo de domínio que orquestrará outros objetos de modelo de domínio. Deixaremos o
IDE IntelliJ criar a classe Game vazia :
pacote com.wordz.domain;
Essa é outra vantagem do TDD. Quando escrevemos o teste primeiro, fornecemos ao nosso IDE informações
suficientes para poder gerar código padrão para nós. Ativamos o recurso de preenchimento automático do
IDE para realmente nos ajudar. Se o seu IDE não puder gerar código automaticamente depois de escrever
o teste, considere atualizar seu IDE.
3. A próxima etapa é adicionar um método start() na classe do controlador para iniciar um novo jogo.
Precisamos saber para qual jogador estamos iniciando o jogo, então passamos um objeto Player .
Escrevemos a etapa Act do nosso teste:
void iniciaNovoJogo() {
var jogo = novo jogo();
var jogador = novo jogador();
jogo.start(jogador);
}
}
Machine Translated by Google
A palavra selecionada e o número da tentativa atual precisarão persistir em algum lugar. Usaremos o padrão de
repositório para abstrair isso. Nosso repositório precisará gerenciar alguns objetos de domínio. Esses objetos terão
a única responsabilidade de rastrear nosso progresso no jogo.
Já vemos um benefício do TDD em termos de feedback rápido do design. Ainda não escrevemos muito
código, mas já parece que a nova classe necessária para monitorar o progresso do jogo seria melhor
chamada de classe Game. Porém, já temos uma classe Game, responsável por iniciar um novo jogo. O
TDD está fornecendo feedback sobre nosso design – que nossos nomes e responsabilidades são incompatíveis.
• Manter nossa classe Game existente como está. Chame esta nova classe de algo como Progresso
ou tentativa.
• Altere o método start() para um método estático – um método que se aplica a todas as instâncias de
uma aula.
• Renomeie a classe Jogo para algo que melhor descreva sua responsabilidade. Então, podemos criar uma
nova classe Game para manter o progresso do jogador atual.
A opção do método estático não é atraente. Ao usar programação orientada a objetos em Java, os métodos
estáticos raramente parecem tão adequados quanto simplesmente criar outro objeto que gerencie todas as
instâncias relevantes. O método estático se torna um método normal neste novo objeto. Usar a classe Game para
representar o progresso de um jogo parece resultar em um código mais descritivo. Vamos com essa abordagem.
1. Use o IDE IntelliJ IDEA para refatorar/renomear a classe Game class Wordz, que representa o ponto de
entrada em nosso modelo de domínio. Também renomeamos a variável local game para corresponder:
palavraz.start(jogador);
}
}
O nome do teste NewGameTest ainda é bom. Representa a história do usuário que estamos testando e
não está relacionada a nenhum nome de classe. O código de produção também foi refatorado pelo IDE:
2. Use o IDE para refatorar/renomear o método start() newGame(). Isto parece descrever
melhor a responsabilidade do método, no contexto de uma classe chamada Wordz:
void iniciaNovoJogo() {
var palavraz = new Palavraz();
var jogador = novo jogador();
wordz.newGame(jogador);
}
}
3. Ao iniciarmos um novo jogo, precisamos selecionar uma palavra para adivinhar e iniciar a sequência de tentativas
que o jogador tem. Esses fatos precisam ser armazenados em um repositório. Vamos criar o repositório
primeiro. Vamos chamá-la de interface GameRepository e adicionar suporte Mockito @Mock para ela em nosso teste:
pacote com.wordz.domain;
importar org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
@InjectMocks
palavra privada Wordz;
@Teste
void iniciaNovoJogo() {
var jogador = novo jogador();
wordz.newGame(jogador);
}
}
Adicionamos a anotação @ExtendWith à classe para permitir que a biblioteca Mockito crie
automaticamente duplicatas de teste para nós. Adicionamos um campo gameRepository , que
anotamos como Mockito @Mock. Usamos a anotação de conveniência @InjectMocks incorporada
ao Mockito para injetar automaticamente essa dependência no construtor Wordz .
pacote com.wordz.domain;
5. Para a próxima etapa, confirmaremos se gameRepository foi usado. Decidimos adicionar um método
create() na interface, que usa uma instância do objeto da classe Game como seu único parâmetro.
Queremos inspecionar a instância do objeto da classe Game, então adicionamos um capturador de
argumentos. Isso nos permite afirmar sobre os dados do jogo contidos nesse objeto:
void iniciaNovoJogo() {
Machine Translated by Google
wordz.newGame(jogador);
var gameArgument =
ArgumentCaptor.forClass(Game.class)
verificar (gameRepositório)
.create(gameArgument.capture());
var jogo = gameArgument.getValue();
assertThat(game.getWord()).isEqualTo("ARISE");
assertThat(game.getAttemptNumber()).isZero();
assertThat(game.getPlayer()).isSameAs(player);
}
}
Uma boa pergunta é por que estamos nos posicionando contra esses valores específicos. A razão é que
vamos trapacear quando adicionarmos o código de produção e falsificá-lo até conseguirmos. Retornaremos
um objeto Game que codifica esses valores como uma primeira etapa. Podemos então trabalhar em
pequenos passos. Depois que a versão do cheat passar no teste, podemos refinar o teste e testar o código
para buscar a palavra de verdade. Etapas menores fornecem feedback mais rápido. O feedback rápido
permite uma melhor tomada de decisão.
Geralmente isso não é recomendado. Isso pode fazer com que lógica importante seja colocada em
outras classes – um cheiro de código conhecido como método estrangeiro. A programação orientada a
objetos trata da co-localização de lógica e dados, encapsulando ambos. Os getters devem ser poucos e
distantes entre si. Isso não significa que nunca devemos usá-los.
Neste caso, a única responsabilidade da classe Game é transferir o estado atual do jogo que está
sendo jogado para o GameRepository. A maneira mais direta de implementar isso é adicionar getters
à classe. Escrever código simples e claro é melhor do que seguir regras dogmaticamente.
6. Criamos métodos vazios para esses novos getters usando o IDE. A próxima etapa é executar o
NewGameTest e confirmar se ele falhou:
pacote com.wordz.domain;
O teste agora passa. Podemos passar da nossa fase vermelho-verde para pensar em
refatoração. O que chama a atenção imediatamente é o quão ilegível é o código do
ArgumentCaptor no teste. Ele contém muitos detalhes sobre a mecânica da zombaria e poucos
detalhes sobre por que estamos usando essa técnica. Podemos esclarecer isso extraindo um método bem nomeado.
@Teste
void iniciaNovoJogo() {
var jogador = novo jogador();
wordz.newGame(jogador);
Isso tornou o teste muito mais simples de ler e ver o padrão usual de Arrange, Act e Assert nele. É um
teste simples por natureza e deve ser lido como tal. Agora podemos executar novamente o teste e
confirmar se ele ainda passa. Sim, e estamos satisfeitos porque nossa refatoração não quebrou nada.
Isso conclui nosso primeiro teste – um trabalho bem executado! Estamos fazendo bons progressos aqui. Sempre é bom
para mim ver um teste ficar verde, e esse sentimento nunca envelhece. Este teste é essencialmente um teste ponta a
ponta de uma história de usuário, atuando apenas no modelo de domínio. O uso da arquitetura hexagonal nos permite
escrever testes que cobrem os detalhes da lógica de nossa aplicação, evitando a necessidade de ambientes de teste.
Como resultado, obtemos testes mais rápidos e mais estáveis.
Há mais trabalho a ser feito em nosso próximo teste, pois precisamos remover a criação codificada do Jogo
objeto. Na próxima seção, abordaremos isso triangulando a lógica de seleção de palavras. Projetamos o próximo teste
para eliminar o comportamento correto de selecionar uma palavra aleatoriamente.
A próxima tarefa é remover a trapaça que usamos para fazer o teste anterior passar. Codificamos alguns dados quando
criamos um objeto Game . Precisamos substituir isso pelo código correto. Este código deve selecionar uma palavra
aleatoriamente do nosso repositório de palavras conhecidas de cinco letras.
1. Adicione um novo teste para eliminar o comportamento de selecionar uma palavra aleatória:
@Teste
void selecionaPalavraRandom() {
2. A seleção aleatória de palavras depende de dois sistemas externos – o banco de dados que contém as palavras
para escolher e uma fonte de números aleatórios. Como estamos usando arquitetura hexagonal, a camada de
domínio não pode acessá-los diretamente. Iremos representá-los com duas interfaces – as portas para esses
sistemas. Para este teste, usaremos o Mockito para criar stubs para essas interfaces:
@ExtendWith(MockitoExtension.class)
@Zombar
@Zombar
@InjectMocks
palavra privada Wordz;
Este teste apresenta dois novos objetos colaboradores à classe Wordz. Estas são
instâncias de quaisquer implementações válidas da interface WordRepository e da
interface RandomNumbers. Precisamos injetar esses objetos no objeto Wordz para utilizá-los.
3. Usando injeção de dependência, injete os dois novos objetos de interface no construtor da classe Wordz :
Adicionamos dois parâmetros ao construtor. Não precisamos armazená-los diretamente como campos.
Em vez disso, usamos a classe WordSelection criada anteriormente. Criamos uma seleção de palavras
objeto e armazená-lo em um campo chamado wordSelection. Observe que nosso uso anterior de @InjectMocks
significa que nosso código de teste passará automaticamente os objetos simulados para este construtor,
sem mais alterações no código. É muito conveniente.
@Teste
void selecionaPalavraRandom() {
quando(randomNumbers.next(anyInt())).thenReturn(2);
quando(wordRepository.fetchWordByNumber(2))
.thenReturn("ABCDE");
Isso configurará nossos mocks para que quando next() for chamado, ele retorne a palavra número 2
todas as vezes, como um teste duplo para o número aleatório que será produzido na aplicação completa.
Quando fetchWordByNumber() é chamado com 2 como argumento, ele retornará a palavra
Machine Translated by Google
com a palavra número 2, que será "ABCDE" em nosso teste. Olhando para esse código, podemos
adicionar clareza usando uma variável local em vez do número mágico 2. Para futuros leitores do
código, a ligação entre a saída do gerador de números aleatórios e o repositório de palavras será mais óbvia:
@Teste
void selecionaPalavraRandom() {
int palavraNúmero = 2;
quando(randomNumbers.next(anyInt()))
.thenReturn(palavraNumber);
quando(palavraRepositório
.fetchWordByNumber(palavraNumber))
.thenReturn("ABCDE");
5. Isso ainda parece muito detalhado mais uma vez. Há muita ênfase na mecânica de zombaria e muito pouca
no que a zombaria representa. Vamos extrair um método para explicar por que estamos configurando este
esboço. Também passaremos a palavra que queremos que seja selecionada. Isso nos ajudará a entender
mais facilmente o propósito do código de teste:
@Teste
void selecionaPalavraRandom() {
dadoWordToSelect("ABCDE");
}
quando(randomNumbers.next(anyInt())) .thenReturn(wordNumber);
quando(palavraRepositório
.fetchWordByNumber(palavraNumber)) .thenReturn(palavraToSelect);
}
Machine Translated by Google
6. Agora, podemos escrever a afirmação para confirmar que esta palavra foi passada para o método
gameRepository create() – podemos reutilizar nosso método auxiliar de afirmação getGameInRepository() :
@Teste
void selecionaPalavraRandom() {
dadoWordToSelect("ABCDE");
7. Observe o teste falhar. Escreva o código de produção para fazer o teste passar:
Nosso teste inicial falhou. Quebramos algo durante nossa última alteração de código. O TDD nos
manteve seguros fornecendo um teste de regressão para nós. O que aconteceu é que depois de
remover a palavra codificada "ARISE" na qual o teste original se baseava, ele falhou. A solução correta é
Machine Translated by Google
adicione a configuração simulada necessária ao nosso teste original. Podemos reutilizar nosso dadoWordToSelect()
método auxiliar para fazer isso.
@Teste
void iniciaNovoJogo() {
var jogador = novo jogador();
dadoWordToSelect("ARISE");
wordz.newGame(jogador);
Testamos nosso primeiro trecho de código para iniciar um novo jogo, com uma palavra selecionada aleatoriamente
para adivinhar, e fizemos os testes passarem. Antes de prosseguirmos, é hora de considerar o que – se houver
alguma coisa – devemos refatorar. Estamos organizando o código enquanto o escrevemos, mas há um recurso
evidente. Dê uma olhada nos dois testes. Eles parecem muito semelhantes agora. O teste original tornou-se um
superconjunto daquele que usamos para testar a adição da seleção de palavras. O teste selectsRandomWord() é um
teste de andaime que não serve mais a nenhum propósito. Só há uma coisa a fazer com um código como esse:
removê-lo. Como uma pequena melhoria na legibilidade, também podemos extrair uma constante para a variável Player :
@Teste
Machine Translated by Google
void iniciaNovoJogo() {
dadoWordToSelect("ARISE");
palavraz.newGame(PLAYER);
2. Executaremos todos os testes depois disso para garantir que todos ainda passem e que
selectsRandomWord() tenha desaparecido.
É isso! Testamos todo o comportamento necessário para iniciar um jogo. É uma conquista significativa
porque o teste cobre uma história de usuário completa. Toda a lógica do domínio foi testada e está
funcionando. O design parece simples. O código de teste é uma especificação clara do que esperamos
que nosso código faça. Este é um grande progresso.
Após essa refatoração, podemos passar para a próxima tarefa de desenvolvimento – código que dá
suporte ao jogo.
Jogando o jogo
Nesta seção, construiremos a lógica para jogar. A jogabilidade consiste em fazer uma série de palpites na
palavra selecionada, revisar a pontuação desse palpite e dar outro palpite. O jogo termina quando a palavra
for adivinhada corretamente ou quando o número máximo de tentativas permitidas for feito.
Começaremos assumindo que estamos no início de um jogo típico, prestes a dar o nosso primeiro palpite.
Também assumiremos que esta suposição não está completamente correta. Isto permite-nos adiar decisões
sobre o comportamento no final do jogo, o que é bom, pois já temos o suficiente para decidir.
Machine Translated by Google
Claramente, a informação mais importante para o jogador é a pontuação do palpite atual. Sem isso, o jogo não
pode ser jogado. Como o jogo tem duração variável – terminando quando a palavra foi adivinhada ou quando um
número máximo de palpites foi tentado – precisamos de um indicador de que outro palpite será permitido.
A ideia por trás do retorno do histórico de pontuações para suposições anteriores é que isso pode ajudar o consumidor
de nosso modelo de domínio - em última análise, algum tipo de interface de usuário. Se retornarmos apenas a
pontuação da estimativa atual, a interface do usuário provavelmente precisará reter seu próprio histórico de
pontuações, para apresentá-las adequadamente. Se retornarmos todo o histórico de pontuações deste jogo, essa
informação estará facilmente disponível. Uma boa regra geral em software é seguir o princípio de que você não vai precisar dele (YAGNI)
Como não há exigência de histórico de pontuações, não iremos construí-lo nesta fase.
A última decisão que precisamos para escrever nosso teste é pensar na interface de programação que
queremos para isso. Escolheremos um método Assessment() na classe Wordz. Ele aceitará String, que é a
estimativa atual do jogador. Ele retornará record, que é uma maneira moderna de Java (desde Java 14) de
indicar que uma estrutura de dados pura deve ser retornada:
Agora temos o suficiente para escrever um teste. Faremos um novo teste para todos os comportamentos relacionados a
suposições, chamado class GuessTest. O teste fica assim:
@ExtendWith(MockitoExtension.class)
@Zombar
@InjectMocks
palavra privada Wordz;
Machine Translated by Google
@Teste
void retornaScoreForGuess() {
dadoGameInRepository(
Game.create(PLAYER, CORRECT_WORD));
.isEqualTo(Carta.PART_CORRECT);
}
.fetchForPlayer(eq(PLAYER)))
.thenReturn(Opcional.de(jogo));
}
}
Não há novas técnicas de TDD no teste. Isso elimina a interface de chamada do nosso novo
métodoassess() . Usamos o idioma do construtor estático para criar o objeto do jogo usando Game.create().
Este método foi adicionado à classe Game:
static Game create(Player player, String correctWord) { return new Game(player, correctWord, 0,
false);
Isso esclarece as informações necessárias para criar um novo jogo. Para compilar o teste, criamos
registrar palpite resultado:
pacote com.wordz.domain;
importar java.util.List;
){}
Machine Translated by Google
Podemos fazer o teste passar escrevendo o código de produção para o método Assessment()
na classe Wordz. Para fazer isso, reutilizaremos a classe Word que já escrevemos:
A afirmação verifica apenas se a pontuação da primeira letra está correta. Este é intencionalmente um teste fraco.
O teste detalhado do comportamento de pontuação é feito na classe WordTest, que escrevemos anteriormente.
O teste é descrito como fraco, pois não testa totalmente a pontuação retornada, apenas a primeira letra
dela. Testes fortes da lógica de pontuação acontecem em outro lugar, na classe WordTest. O teste fraco
aqui confirma que temos algo capaz de pontuar pelo menos uma letra corretamente e é o suficiente para
testarmos o código de produção. Evitamos duplicar testes aqui.
A execução do teste mostra que ele passou. Podemos revisar o código de teste e o código de produção
para ver se a refatoração melhorará seu design. Neste ponto, nada precisa da nossa atenção urgente.
Podemos seguir acompanhando o progresso do jogo.
Precisamos acompanhar o número de palpites que foram feitos para que possamos encerrar o jogo após um
número máximo de tentativas. Nossa escolha de design é atualizar o campo tryNumber no objeto Game e
então armazená-lo no GameRepository:
@Teste
void atualizaçõesAttemptNumber() {
dadoGameInRepository(
Game.create(PLAYER, CORRECT_WORD));
palavraz.avaliar(PLAYER, WRONG_WORD);
Argumento ArgumentCaptor<Jogo>
= ArgumentCaptor.forClass(Game.class);
verificar(gameRepository).update(argument.capture());
retornar argumento.getValue();
}
Este teste introduz um novo método, update(), em nossa interface GameRepository, responsável
por gravar as informações mais recentes do jogo no armazenamento. A etapa Assert usa um
Mockito ArgumentCaptor para inspecionar o objeto Game que passamos para update().
Escrevemos um método getUpdatedGameInRepository() para tirar a ênfase do funcionamento
interno de como verificamos o que foi passado para o método gameRepository.update() .
assertThat() no teste verifica se o tryNumber foi incrementado. Começou do zero, devido à
criação de um novo jogo, e portanto o novo valor esperado é 1. Este é o comportamento
desejado para rastrear uma tentativa de adivinhar a palavra:
pacote com.wordz.domain;
O teste agora passa. Podemos pensar em quaisquer melhorias de refatoração que queiramos fazer. Há
duas coisas que parecem se destacar:
Nesta fase, podemos conviver com esta duplicação. As opções são combinar ambos os testes na
mesma classe de teste, estender uma classe base de teste comum ou usar composição. Nenhum deles
parece ajudar muito na legibilidade. Parece muito bom ter os dois casos de teste diferentes separados
por enquanto.
• As três linhas dentro do métodoasset() sempre devem ser chamadas como uma unidade quando
tentamos outra estimativa. É possível esquecer de chamar um deles, então parece melhor refatorar
para eliminar esse possível erro. Podemos refatorar assim:
Movemos o código que estava aqui para o método recém-criado: try() na classe
Game:
Isso completa o código necessário para adivinhar a palavra. Vamos testar o código que precisaremos para
detectar quando um jogo termina.
Terminando o jogo
Nesta seção, concluiremos os testes e o código de produção necessários para detectar o final de um jogo.
Isso acontecerá quando fizermos um dos seguintes:
Podemos começar codificando a detecção de fim de jogo quando adivinhamos a palavra corretamente.
Machine Translated by Google
@Teste
void relatóriosGameOverOnCorrectGuess(){
var jogador = novo jogador();
Jogo jogo = novo Jogo(jogador, "ARISE", 0);
quando(gameRepository.fetchForPlayer(player))
.thenReturn(jogo);
var palavraz = new Wordz(gameRepository,
wordRepositório, números aleatórios);
assertThat(result.isGameOver()).isTrue();
public GuessResult avaliar (Player player, String palpite) { var game = gameRepository.fetchForPlayer
(player);
Pontuação pontuação = game.attempt(palpite); if
(pontuação.allCorrect()) {
retornar novo GuessResult(pontuação, verdadeiro);
gameRepository.update(jogo);
retornar novo GuessResult(pontuação, falso);
@Teste
void relatóriosAllCorrect() {
Machine Translated by Google
@Teste
void relatóriosNotAllCorrect() {
var palavra = new Palavra("ARISE");
var pontuação = word.guess("ARI*E");
assertThat(score.allCorrect()).isFalse();
}
Com isso, temos uma implementação válida para o acessador isGameOver no registro GuessResult.
Todos os testes passam. Nada parece precisar de refatoração. Passaremos para o próximo teste.
O próximo teste eliminará a resposta ao exceder o número máximo de suposições permitidas em um jogo:
@Teste
void gameOverOnTooManyIncorrectGuesses(){
int máximoGuesses = 5;
dadoGameInRepository(
Game.create(PLAYER, CORRECT_WORD,
máximoGuesses-1));
assertThat(result.isGameOver()).isTrue();
Este teste configura gameRepository para permitir uma estimativa final. Em seguida, ele configura a suposição como incorreta.
Afirmamos que isGameOver() é verdadeiro neste caso. O teste falha inicialmente, conforme desejado. Adicionamos
um método construtor estático extra na classe Game para especificar um número inicial de tentativas.
Adicionamos o código de produção para encerrar o jogo com base em um número máximo de suposições:
Todos os nossos testes agora passam. Há algo suspeito no código, no entanto. Ele foi muito bem ajustado para
funcionar apenas se uma estimativa estiver correta e dentro do número permitido de estimativas, ou quando a
estimativa estiver incorreta e exatamente no número permitido. É hora de adicionar alguns testes de condições de
contorno e verificar novamente nossa lógica.
Precisamos de mais alguns testes em torno das condições de limite da detecção de jogo. O primeiro elimina a
resposta a uma estimativa incorreta enviada após uma estimativa correta:
@Teste
void rejeitaGuessAfterGameOver(){
var gameOver = novo jogo(PLAYER, CORRECT_WORD,
1, verdadeiro);
Machine Translated by Google
dadoGameInRepository(gameOver);
assertThat(result.isError()).isTrue();
}
• Este novo campo deverá ser definido sempre que o jogo terminar. Precisaremos de mais testes para dirigir
esse comportamento.
Isso leva a um pouco de refatoração automatizada para adicionar o quarto parâmetro ao construtor da classe
Game . Então, podemos adicionar código para fazer o teste passar:
if(game.isGameOver()) {
retornar GuessResult.ERROR;
}
A decisão de design aqui é que assim que buscarmos o objeto Game , verificaremos se o jogo foi previamente
marcado como encerrado. Nesse caso, reportamos um erro e pronto. É simples e grosseiro, mas adequado aos
nossos propósitos. Também adicionamos uma constante estática, GuessResult.ERROR, para facilitar a leitura:
Uma consequência desta decisão de design é que devemos atualizar o GameRepository sempre que o
campo Game.isGameOver mudar para verdadeiro. Um exemplo de um desses testes é este:
@Teste
void recordsGameOverOnCorrectGuess()
{dadoGameInRepository(Game.create(PLAYER, CORRECT_WORD));
palavraz.assess(PLAYER, CORRECT_WORD);
if(game.isGameOver()) {
retornar GuessResult.ERROR;
}
gameRepository.update(jogo);
gameRepository.update(jogo);
retornar novo GuessResult(pontuação,
Machine Translated by Google
game.hasNoRemainingGuesses(), falso);
}
Precisamos de outro teste para eliminar a gravação do fim do jogo quando ficarmos sem palpites. Isso levará
a uma mudança no código de produção. Essas alterações podem ser encontradas no GitHub no link
fornecido no início deste capítulo. Eles são muito semelhantes aos feitos anteriormente.
Finalmente, vamos revisar nosso design e ver se podemos melhorá-lo ainda mais.
Os testes que já escrevemos nos permitem grande latitude na refatoração. Eles evitaram testar implementações
específicas, em vez disso testaram os resultados desejados. Eles também testam unidades de código maiores –
neste caso, o modelo de domínio da nossa arquitetura hexagonal. Como resultado, sem alterar nenhum teste, é
possível refatorar nossa classe Wordz para ficar assim:
pacote com.wordz.domain;
if(game.isGameOver()) {
retornar GuessResult.ERROR;
gameRepository.update(jogo);
retornar novo GuessResult(pontuação,
game.isGameOver(), falso);
}
}
Isso parece mais simples. O código do construtor da classe GuessResult agora se destaca por ser
particularmente feio. Ele apresenta o antipadrão clássico de usar vários valores de sinalizadores booleanos.
Precisamos esclarecer o que realmente significam as diferentes combinações, para simplificar a criação do
objeto. Uma abordagem útil é aplicar o idioma do construtor estático mais uma vez:
pacote com.wordz.domain;
){
ERRO de GuessResult final estático
booleano isGameOver) {
retornar novo GuessResult(pontuação, isGameOver, false);
}
}
Machine Translated by Google
Isso simplifica o método Assessment() eliminando a necessidade de entender o sinalizador booleano final:
if(game.isGameOver()) {
retornar GuessResult.ERROR;
}
gameRepository.update(jogo);
Outra melhoria para auxiliar a compreensão diz respeito à criação de novas instâncias da classe Game. O teste
rejeitaGuessAfterGameOver() usa valores de sinalizadores booleanos em um construtor de quatro argumentos
para configurar o teste em um estado de fim de jogo. Vamos tornar explícito o objetivo de criar um estado
de fim de jogo. Podemos tornar o construtor Game privado e aumentar a visibilidade do método end() , que
já é usado para encerrar um jogo. Nosso teste revisado fica assim:
@Teste
void rejeitaGuessAfterGameOver(){
var jogo = Game.create(PLAYER, CORRECT_WORD);
game.end();
dadoGameInRepository(jogo);
assertThat(result.isError()).isTrue();
}
A etapa Organizar agora é mais descritiva. O construtor de quatro argumentos não está mais acessível,
orientando o desenvolvimento futuro para usar métodos de construtor estático mais seguros e descritivos.
Esse design aprimorado ajuda a evitar a introdução de defeitos no futuro.
Fizemos grandes progressos neste capítulo. Após essas melhorias finais de refatoração, temos uma descrição
facilmente legível da lógica central do nosso jogo. É totalmente apoiado pelos testes de unidade FIRST. Nós
Machine Translated by Google
Resumo 263
alcançamos até uma cobertura significativa de código de 100% das linhas de código executadas por nossos testes. Isso é
mostrado na ferramenta de cobertura de código IntelliJ:
Esse é o núcleo do nosso jogo finalizado. Podemos começar um novo jogo, jogar e terminar um jogo. O jogo pode ser
desenvolvido para incluir recursos como a atribuição de uma pontuação com base na rapidez com que a palavra foi
adivinhada e uma tabela de pontuação para os jogadores. Eles seriam adicionados usando as mesmas técnicas que
aplicamos ao longo deste capítulo.
Resumo
Cobrimos muito terreno neste capítulo. Usamos TDD para eliminar a lógica principal do aplicativo em nosso
jogo Wordz. Demos pequenos passos e usamos a triangulação para inserir cada vez mais detalhes em
nossa implementação de código. Usamos arquitetura hexagonal para nos permitir usar testes de unidade
FIRST, livrando-nos de testes de integração complicados com seus ambientes de teste. Empregamos
testes duplos para substituir objetos difíceis de controlar, como o banco de dados e a geração de números aleatórios.
Construímos um conjunto valioso de testes unitários que são dissociados de implementações específicas. Isso nos permitiu
refatorar o código livremente, resultando em um design de software muito bom, baseado nos princípios SOLID, o que
reduzirá significativamente os esforços de manutenção.
Terminamos com um relatório significativo de cobertura de código que mostrou que 100% das linhas de código de produção
foram executadas por nossos testes, dando-nos um alto grau de confiança em nosso trabalho.
A seguir, no Capítulo 14, Conduzindo a camada de banco de dados, escreveremos o adaptador de banco de dados junto
com um teste de integração para implementar nosso GameRepository, usando o banco de dados Postgres.
Machine Translated by Google
Perguntas e respostas
1. Cada método em cada classe precisa ter seu próprio teste unitário?
Não. Essa parece ser uma visão comum, mas é prejudicial. Se usarmos essa abordagem, estaremos bloqueando os
detalhes da implementação e não seremos capazes de refatorar sem interromper os testes.
Não muito, por si só. Significa simplesmente que todas as linhas de código nas unidades em teste foram executadas
durante a execução do teste. Para nós, isso significa um pouco mais devido ao uso do TDD test-first. Sabemos que
cada linha de código foi orientada por um teste significativo de comportamento que é importante para nossa aplicação.
Ter 100% de cobertura é uma verificação dupla de que não esquecemos de adicionar um teste.
3. 100% de cobertura do código durante a execução do teste significa que temos um código perfeito?
Não. Os testes só podem revelar a presença de defeitos, nunca a sua ausência. Podemos ter 100% de cobertura com
código de qualidade muito baixa em termos de legibilidade e tratamento de casos extremos. É importante não atribuir
muita importância às métricas de cobertura de código. Para TDD, eles servem como uma verificação cruzada de que
não perdemos nenhum teste de condição de contorno.
Sim. TDD tem tudo a ver com ciclos de feedback rápidos. O feedback nos ajuda a explorar ideias de design e a mudar
de ideia à medida que descobrimos designs melhores. Isso nos liberta da tirania de ter que entender cada detalhe – de
alguma forma – antes de começarmos a trabalhar. Descobrimos um design fazendo o trabalho e temos um software
funcional para mostrá-lo no final.
Leitura adicional
• Documentação do AssertJ – leia mais sobre os vários tipos de matchers de asserções incorporados ao AssertJ, bem como detalhes
github.io/doc/.
• Refatoração – Melhorando o Design do Código Existente, Martin Fowler (primeira edição),
ISBN 9780201485677:
A maior parte do nosso trabalho em TDD é refatorar código, fornecendo continuamente um design suficientemente bom
para suportar nossos novos recursos. Este livro contém excelentes conselhos sobre como abordar a refatoração de
maneira disciplinada e passo a passo.
A primeira edição do livro usa Java em todos os seus exemplos, portanto é mais útil para nós do que a segunda edição
baseada em JavaScript.
Machine Translated by Google
• Padrões de Projeto – Elementos de Software Orientado a Objetos Reutilizáveis, Gamma, Helm, Vlissides,
Johnson, ISBN 9780201633610:
Um livro de referência que catalogou combinações comuns de classes que ocorrem em software orientado a
objetos . Anteriormente neste capítulo, usamos uma classe controladora. Isso é descrito como um padrão de
fachada, nos termos deste livro. Os padrões listados são livres de qualquer tipo de estrutura ou camada de
software e, portanto, são muito úteis na construção do modelo de domínio da arquitetura hexagonal.
Machine Translated by Google
Machine Translated by Google
14
Conduzindo a camada de banco de dados
Neste capítulo, implementaremos um adaptador de banco de dados para uma de nossas portas no modelo de domínio,
representado pela interface WordRepository . Isso permitirá que nosso modelo de domínio busque palavras para adivinhar
em um banco de dados real, neste caso, usando o popular banco de dados de código aberto Postgres. Testaremos a
configuração do banco de dados e o código que acessa o banco de dados. Para nos ajudar a fazer isso, usaremos uma
estrutura de teste projetada para simplificar a escrita de testes de integração de banco de dados, chamada DBRider.
Ao final do capítulo, teremos escrito um teste de integração em um banco de dados em execução, implementado o
método fetchesWordByNumber() da interface WordRepository e usado a biblioteca de acesso ao banco de dados
JDBI para nos ajudar. Criaremos um usuário de banco de dados com permissões em uma tabela armazenando
palavras para adivinhar. Criaremos essa tabela e, em seguida, escreveremos uma consulta SQL que o JDBI usará
para recuperar a palavra que procuramos. Usaremos uma consulta SQL de parâmetro nomeado para evitar alguns
problemas de segurança do aplicativo causados por injeções de SQL.
Requerimentos técnicos
O código final deste capítulo pode ser encontrado em https://github.com/PacktPublishing/
Desenvolvimento orientado a testes com Java/tree/main/chapter14.
O código foi testado com a versão 14.5. Espera-se que funcione em todas as versões.
Machine Translated by Google
Com a configuração concluída, vamos começar a implementar o código do nosso banco de dados. Na próxima seção,
usaremos a estrutura DBRider para criar um teste de integração de banco de dados.
Nesta seção, criaremos o esqueleto de um teste de integração de banco de dados usando uma estrutura de teste chamada
DBRider. Usaremos este teste para eliminar a criação de uma tabela de banco de dados e de um usuário de banco de dados. Vamos
Anteriormente, criamos um modelo de domínio para nosso aplicativo Wordz, usando arquitetura hexagonal para nos
guiar. Em vez de acessar diretamente um banco de dados, nosso modelo de domínio usa uma abstração, conhecida
como porta na terminologia hexagonal. Uma dessas portas é a interface WordRepository , que representa palavras
armazenadas para adivinhação.
As portas devem sempre ser implementadas por adaptadores em arquitetura hexagonal. Um adaptador para
a interface WordRepository será uma classe que implementa a interface, contendo todo o código necessário
para acessar o banco de dados real.
Para testar esse código do adaptador, escreveremos um teste de integração, usando uma biblioteca que oferece
suporte a bancos de dados de teste. A biblioteca se chama DBRider e é uma das dependências listadas no gradle do projeto.
arquivo de construção :
dependências {
testImplementation 'org.junit.jupiter:junit-jupiter api:5.8.2'
testImplementation 'org.assertj:assertj-core:3.22.0'
testImplementation 'org.mockito:mockito-core:4.8.0'
testImplementation 'org.mockito:mockito-junit jupiter:4.8.0'
testImplementation 'com.github.database-rider:rider
núcleo: 1,33,0'
implementação 'org.postgresql:postgresql:42.5.0'
}
Machine Translated by Google
O DBRider possui uma biblioteca chamada rider-junit5, que se integra ao JUnit5. Com essas novas ferramentas de
teste, podemos começar a escrever nosso teste. A primeira coisa a fazer é configurar o teste para que ele use o
DBRider para se conectar ao nosso banco de dados Postgres.
Antes de testarmos qualquer código de aplicativo, precisaremos de um teste conectado ao nosso banco de dados Postgres,
executado localmente. Começamos da maneira usual, escrevendo uma classe de teste JUnit5:
@DBRider
@DBUnit(caseSensitiveTableNames = verdadeiro,
caseInsensitiveStrategy= Ortografia.LOWERCASE)
Os parâmetros na anotação @DBUnit atenuam algumas interações estranhas entre o Postgres e a estrutura de
teste DBRider relacionadas à diferenciação de maiúsculas e minúsculas em nomes de tabelas e colunas.
3. Queremos testar se uma palavra pode ser obtida. Adicione um método de teste vazio:
@Teste
void buscaPalavra() {
}
Machine Translated by Google
5. A próxima etapa para corrigir isso é seguir a documentação do DBRider e adicionar o código que
será usado pela estrutura DBRider. Adicionamos um campo connectionHolder e um campo
javax. campo sqlDataSource para suportar isso:
@DBRider
O dataSource é a forma JDBC padrão de criar uma conexão com nosso banco de dados Postgres.
Executamos o teste. Ele falha com uma mensagem de erro diferente:
@BeforeEach
void setupConnection() {
var ds = new PGSimpleDataSource();
ds.setServerNames(new String[]{"localhost"});
ds.setDatabaseName("palavrazdb");
ds.setCurrentSchema("público"); ds.setUser("ciuser");
Machine Translated by Google
ds.setPassword("cipassword");
this.dataSource = ds;
}
Isso especifica que queremos que um usuário chamado ciuser com a senha cipassword se conecte
a um banco de dados chamado wordzdb, rodando em localhost na porta padrão do Postgres (5432).
O erro é causado porque ainda não temos um usuário ciuser conhecido em nosso banco de dados
Postgres . Vamos criar um.
Ele falha porque a estrutura DBRider está tentando conectar nosso novo usuário ciuser ao banco
de dados wordzdb . Este banco de dados não existe.
Machine Translated by Google
O teste fetchesWord() agora passa. Lembramos que o método de teste em si está vazio, mas isso significa que
temos banco de dados suficiente configurado para prosseguir com o teste do código de produção. Retornaremos
à configuração do banco de dados em breve, mas permitiremos que nosso test-drive nos guie. A próxima tarefa
é adicionar o código Arrange, Act e Assert ausente ao teste fetchesWord() .
@Teste
Queremos verificar se podemos buscar a palavra ARISE no banco de dados. Este teste falha. Nós precisamos
2. Queremos que nossa nova classe adaptadora implemente a interface WordRepository , então
eliminamos isso na etapa Organizar do nosso teste:
@Teste
3. Agora deixamos o assistente do IDE fazer a maior parte do trabalho na criação de nossa nova classe de
adaptador. Vamos chamá -lo de WordRepositoryPostgres, que liga os dois fatos de que a classe
implementa a interface WordRepository e também implementa o acesso a um banco de dados
Postgres. Usamos o assistente Nova Classe e o colocamos em um novo pacote, com.wordz.adapters.db:
pacote com.wordz.adapters.db;
importar com.wordz.domain.WordRepository;
@Sobrepor
5. Voltando ao nosso teste, podemos adicionar a linha act, que chamará o método fetchWordByNumber() :
@Teste
String real =
repositório.fetchWordByNumber(27);
assertThat(actual).isEqualTo("ARISE");
}
Uma palavra de explicação sobre a misteriosa constante 27 passada para o método fetchWordByNumber() .
Este é um número arbitrário usado para identificar uma palavra específica. Seu único requisito difícil é que
ele esteja alinhado com o número da palavra fornecido nos dados de teste do stub, que veremos um pouco
mais tarde em um arquivo JSON. O valor real de 27 não tem significado além do alinhamento com o
número da palavra dos dados do stub.
@Teste
WordRepositoryPostgres(dataSource);
assertThat(actual).isEqualTo("ARISE");
}
7. A última configuração a ser feita em nosso teste é preencher o banco de dados com a palavra ARISE. Fazemos
isso usando um arquivo JSON que a estrutura DBRider aplicará ao nosso banco de dados na inicialização do teste:
{
"palavra": [
{
"palavra_número": 27,
"palavra": "SURGIR"
}
]
8. Este arquivo deve ser salvo em um local específico para que o DBRider possa localizá-lo. Chamamos o
arquivo wordTable.json e o salvamos no diretório de teste, em /resources/adapters/data:
9. A etapa final na configuração de nosso teste com falha é vincular o arquivo wordTable.json de dados de
teste ao nosso método de teste fetchesWord() . Fazemos isso usando a anotação DBRider @DataSet :
@Teste
@DataSet("adaptadores/ data/wordTable.json")
public void buscaPalavra() {
Repositório WordRepository
= novo WordRepositoryPostgres(dataSource);
String real =
repositório.fetchWordByNumber(27);
assertThat(actual).isEqualTo("ARISE");
}
O teste agora falha e está em uma posição onde podemos fazê-lo passar escrevendo o código de acesso ao banco de dados.
Na próxima seção, usaremos a popular biblioteca JDBI para implementar o acesso ao banco de dados em uma
classe adaptadora para nossa interface WordRepository .
As arquiteturas hexagonais foram abordadas no Capítulo 9, Arquitetura Hexagonal – Desacoplamento de Sistemas Externos.
Um sistema externo, como um banco de dados, é acessado por meio de uma porta no modelo de domínio. O código
específico desse sistema externo está contido em um adaptador. Nosso teste com falha nos permite escrever o código de
acesso ao banco de dados para buscar uma palavra para adivinhar.
Um pouco de pensamento de design de banco de dados precisa ser feito antes de começarmos a escrever o código. Para
a tarefa em questão, basta observar que armazenaremos todas as palavras disponíveis para adivinhar em uma tabela de
banco de dados chamada word. Esta tabela terá duas colunas. Haverá uma chave primária chamada word_number e uma
palavra de cinco letras em uma coluna chamada word.
2. Corrija isso criando uma tabela de palavras no banco de dados. Usamos o console psql para executar o
Comando SQL para criar tabela :
3. Execute o teste novamente. O erro muda para mostrar que nosso usuário ciuser tem permissões insuficientes:
5. Execute o teste novamente. O erro muda para nos mostrar que a palavra não foi lida do
tabela do banco de dados:
Depois de configurar o lado do banco de dados, podemos prosseguir e adicionar o código que acessará o banco
de dados. O primeiro passo é adicionar a biblioteca de banco de dados que usaremos. É JDBI e, para usá-lo,
devemos adicionar a dependência jdbi3-core ao nosso arquivo gradle.build :
dependências {
testImplementation 'org.junit.jupiter:junit-jupiter api:5.8.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-
Machine Translated by Google
motor: 5.8.2'
testImplementation 'org.assertj:assertj-core:3.22.0' testImplementation
'org.mockito:mockito-core:4.8.0'
testImplementation 'org.mockito:mockito-junit jupiter:4.8.0'
testImplementation 'com.github.database-rider:rider
núcleo: 1,35,0'
Observação
Isso nos dá acesso à biblioteca JDBI. Nós organizamos isso para que o JDBI acesse qualquer
DataSource que passarmos para nosso construtor.
2. Adicionamos o código JDBI para enviar uma consulta SQL ao banco de dados e buscar a palavra correspondente
ao wordNumber que fornecemos como parâmetro do método. Primeiro, adicionamos a consulta SQL que usaremos:
+ "palavra_número=:palavraNúmero";
Machine Translated by Google
@Sobrepor
retornar query.mapTo(String.class).one();
});
palavra de retorno;
Nosso teste de integração agora foi aprovado. A classe adaptadora leu a palavra do banco de dados e a retornou.
Implementando GameRepository
O mesmo processo é usado para testar o métodohighWordNumber() e para criar adaptadores para o outro
código de acesso ao banco de dados que implementa a interface GameRepository . O código final deles pode
ser visto no GitHub com comentários para explorar alguns dos problemas nos testes de banco de dados, como
como evitar falhas de teste causadas por dados armazenados.
Há uma etapa manual necessária para testar o código de implementação da interface GameRepository .
Devemos criar uma mesa de jogo .
Machine Translated by Google
Resumo
Neste capítulo, criamos um teste de integração para nosso banco de dados. Usamos isso para testar a implementação de
um usuário de banco de dados, a tabela de banco de dados e o código necessário para acessar nossos dados. Este código
implementou o adaptador para uma de nossas portas em nossa arquitetura hexagonal. Ao longo do caminho, usamos
algumas ferramentas novas. A estrutura de teste do banco de dados DBRider simplificou nosso código de teste. A biblioteca
de acesso ao banco de dados JDBI simplificou nosso código de acesso a dados.
No próximo e último capítulo, Capítulo 15, Conduzindo a Camada Web, adicionaremos uma interface HTTP à nossa
aplicação, transformando-a em um microsserviço completo. Integraremos todos os componentes e, em seguida,
jogaremos nosso primeiro jogo de Wordz usando a ferramenta de teste HTTP Postman.
Perguntas e respostas
1. Devemos automatizar as etapas manuais de criação do banco de dados?
Sim. Esta é uma parte importante do DevOps, onde nós, desenvolvedores, somos responsáveis por
colocar o código em produção e mantê-lo em execução. A técnica principal é Infraestrutura como Código
(IaC), o que significa automatizar etapas manuais como código que fazemos check-in no repositório principal.
Ferramentas populares são Flyway e Liquibase. Ambos nos permitem escrever scripts que são executados na
inicialização do aplicativo e migrarão o esquema do banco de dados de uma versão para outra. Eles auxiliam na
migração de dados através de alterações de esquema quando necessário. Estes estão fora do escopo deste livro.
O acesso a um servidor de banco de dados em execução faz parte da engenharia da plataforma. Para
designs nativos da nuvem executados no Amazon Web Service, Microsoft Azure ou Google Cloud Platform,
use scripts de configuração para essa plataforma. Uma abordagem popular é usar o Terraform da
Hashicorp, que pretende ser uma linguagem de script universal entre provedores para configuração em
nuvem. Isso está fora do escopo deste livro.
Machine Translated by Google
Antes de cada check-in no repositório. Embora os testes de unidade sejam rápidos de executar e devam ser
executados o tempo todo, os testes de integração, por natureza, são mais lentos de executar. É razoável executar
apenas testes unitários enquanto trabalha no código de domínio. Devemos sempre garantir que não quebramos
nada inesperadamente. É aqui que entra a execução de testes de integração. Eles revelam se alteramos
acidentalmente algo que afeta o código da camada do adaptador ou se algo mudou em relação ao layout do
banco de dados.
Leitura adicional
• Flyway é uma biblioteca que nos permite armazenar os comandos SQL para criar e modificar o esquema do nosso
banco de dados como código fonte. Isso nos permite automatizar alterações no banco de dados: https://flywaydb.
organização/
• À medida que o design de nosso aplicativo cresce, nosso esquema de banco de dados precisará mudar. Este site e
os livros que o acompanham descrevem maneiras de fazer isso enquanto gerencia riscos: https://
banco de dadosrefactoring.com/
• Hospedar um banco de dados Postgres na Amazon Web Services usando seu serviço RDS: https://
aws.amazon.com/rds
Machine Translated by Google
Machine Translated by Google
15
Conduzindo a camada da Web
Neste capítulo, completamos nosso aplicativo web adicionando um endpoint web. Aprenderemos como escrever
testes de integração HTTP usando o cliente Java HTTP integrado. Testaremos o código do adaptador da web
que executa esse endpoint, usando uma estrutura de servidor HTTP de código aberto. Este web adaptor é
responsável por converter solicitações HTTP em comandos que podemos executar em nossa camada de domínio.
No final do capítulo, reuniremos todas as peças da nossa aplicação em um microsserviço. O web adaptor e os
adaptadores de banco de dados serão vinculados ao modelo de domínio usando injeção de dependência.
Precisaremos executar alguns comandos manuais de banco de dados, instalar um cliente web chamado Postman
e então poderemos jogar nosso jogo.
• Jogando o jogo
• Integrando o aplicativo
• Usando o aplicativo
Requerimentos técnicos
O código deste capítulo está disponível em https://github.com/PacktPublishing/Test
Driven-Development-with-Java/tree/main/chapter15.
2. Certifique-se de que as etapas de configuração do banco de dados do Capítulo 14, Conduzindo a camada de banco de dados, foram concluídas.
Machine Translated by Google
As seguintes bibliotecas de código aberto serão usadas para nos ajudar a escrever o código:
• Undertow: Este é um servidor web HTTP leve que alimenta a estrutura Molecule
• GSON: esta é uma biblioteca do Google que converte entre objetos Java e dados estruturados JSON
Para começar a construir, primeiro adicionamos as bibliotecas necessárias como dependências ao arquivo
build.gradle . Então podemos começar a escrever um teste de integração para nosso endpoint HTTP e testar a implementação.
dependências {
testImplementation 'org.junit.jupiter:junit-jupiter api:5.8.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter
motor: 5.8.2'
testImplementation 'org.assertj:assertj-core:3.22.0'
testImplementation 'org.mockito:mockito-core:4.8.0'
testImplementation 'org.mockito:mockito-junit jupiter:4.8.0'
testImplementation 'com.github.database-rider:rider
núcleo: 1,35,0'
Machine Translated by Google
implementação 'org.postgresql:postgresql:42.5.0'
implementação 'org.jdbi:jdbi3-core:3.34.0'
implementação 'org.apache.commons:commons-lang3:3.12.0'
implementação 'com.vtence.molecule:molecule:0.15.0'
implementação 'io.thorntail:undertow:2.7.0.Final'
implementação 'com.google.code.gson:gson:2.10'
}
Seguiremos o ciclo normal do TDD para criar nosso web adaptor. Ao escrever testes para objetos na
camada adaptadora, devemos nos concentrar em testar a tradução entre objetos em nossa camada de
domínio e nas comunicações com sistemas externos. Nossa camada adaptadora usará a estrutura Molecule
HTTP para lidar com solicitações e respostas HTTP.
Como utilizamos a arquitetura hexagonal e iniciamos pela camada de domínio, já sabemos que a lógica do
jogo está funcionando. O objetivo deste teste é provar que a camada do adaptador web está cumprindo sua
responsabilidade. Isso significa traduzir solicitações e respostas HTTP para objetos em nossa camada de domínio.
1. Primeiro, escrevemos nossa classe de teste. Vamos chamá-lo de WordzEndpointTest e ele pertence ao grupo com.
Pacote wordz.adapters.api :
pacote com.wordz.adapters.api;
A razão para incluir este pacote é como parte de nossa arquitetura hexagonal. O código neste
adaptador da web pode usar qualquer coisa do modelo de domínio. O próprio modelo de domínio
não tem conhecimento da existência deste web adaptor.
void startJogo() {
}
Machine Translated by Google
2. Este teste precisa capturar a decisão de design que envolve nossa API da web pretendida. Uma decisão
é que, quando um jogo for iniciado com sucesso, retornaremos um simples código de status HTTP 204
No Content . Começaremos com a afirmação para capturar esta decisão:
@Teste
void startJogo() {
Resposta HttpResponse;
afirmarIsso(res)
.hasStatusCode(HttpStatus.NO_CONTENT.code);
}
3. A seguir, escrevemos a etapa Act. A ação aqui é para um cliente HTTP externo enviar uma solicitação
para nosso endpoint da web. Para conseguir isso, usamos o cliente HTTP integrado fornecido pelo próprio Java.
Organizamos o código para enviar a solicitação e depois descartamos qualquer corpo de resposta HTTP, pois
nosso design não retorna um corpo:
@Teste
afirmarIsso(res)
.hasStatusCode(HttpStatus.NO_CONTENT.code);
}
4. A etapa Organizar é onde capturamos nossas decisões sobre a solicitação HTTP a ser enviada. Para iniciar
um novo jogo, precisamos de um objeto Player para identificar o jogador. Enviaremos isso como um Json
objeto no corpo da solicitação . A solicitação causará uma mudança de estado em nosso servidor, então
escolhemos o método HTTP POST para representar isso. Finalmente, escolhemos uma rota cujo caminho é /start:
@Teste
afirmarIsso(res)
.hasStatusCode(HttpStatus.NO_CONTENT.code);
}
Vemos a biblioteca Gson sendo usada para converter um objeto Player em sua representação JSON.
Também vemos que um método POST é construído e enviado para o caminho /start no localhost.
Eventualmente, desejaremos mover os detalhes do localhost para a configuração. Mas, por
enquanto, o teste funcionará em nossa máquina local.
Não é novidade que esse teste falha porque não consegue se conectar a um servidor HTTP. Consertar isso é nossa
próxima tarefa.
Machine Translated by Google
@Teste
Os dois parâmetros passados para o construtor WordzEndpoint definem o host e a porta em que
o endpoint da web será executado.
pacote com.wordz.adapters.api;
Nesse caso, não armazenaremos os detalhes do host e da porta nos campos. Em vez disso,
iniciaremos um WebServer usando uma classe da biblioteca Molecule.
pacote com.wordz.adapters.api;
importar com.vtence.molecule.WebServer;
O código anterior é suficiente para iniciar um servidor HTTP em execução e permitir que o teste se
conecte a ele. Nosso servidor HTTP não faz nada de útil em termos de jogo. Precisamos adicionar
algumas rotas a este servidor junto com o código para respondê-las.
• Que uma rota /start deve ser chamada para iniciar o jogo
• Que usaremos o método HTTP POST
• Que identificaremos a qual jogador o jogo pertence como dados JSON no corpo do POST
1. Teste a rota /start . Para trabalhar em pequenos passos, inicialmente retornaremos um código
de resposta HTTP NOT_IMPLEMENTED :
tente
{ server.route(new Routes() {{ post("/start")
}
}
O teste falha, conforme esperado. Fizemos progressos porque o teste agora falha por um motivo
diferente. Agora podemos nos conectar ao endpoint da web, mas ele não retorna a resposta HTTP
correta. Nossa próxima tarefa é conectar esse endpoint da web ao código da camada de domínio e
realizar as ações relevantes para iniciar um jogo.
1. Adicione o código para chamar a porta da camada de domínio implementada como classe Wordz. Usaremos o
Mockito para criar um teste duplo para este objeto. Isso nos permite testar apenas o código do endpoint da
web, desacoplado de todos os outros códigos:
@ExtendWith(MockitoExtension.class)
@Teste
2. Precisamos fornecer nosso objeto de domínio de classe Wordz para o objeto de classe
WordzEndpoint . Usamos injeção de dependência para injetá-la no construtor:
3. Em seguida, precisamos adicionar o código para iniciar o jogo. Para fazer isso, primeiro extraímos o
objeto Player dos dados JSON no corpo da solicitação . Isso identifica para qual jogador iniciar o jogo.
Então chamamos o método wordz.newGame() . Se for bem-sucedido, retornamos um código
de status HTTP 204 No Content, indicando sucesso:
resposta de retorno
.of(HttpStatus.NO_CONTENT)
.feito();
}
} catch (IOException e) { lançar new
RuntimeException (e);
}
jogue novo
UnsupportedOperationException("Não implementado");
}
Machine Translated by Google
Falha porque o valor de retorno de wordz.newGame() era falso. O objeto simulado precisa ser
configurado para retornar verdadeiro.
@Teste
InterruptedException {
ponto de extremidade var
= novo WordzEndpoint(mockWordz,
"localhost", 8080);
quando(mockWordz.newGame(eq(PLAYER)))
.entãoReturn(verdadeiro);
O teste de integração é aprovado. A solicitação HTTP foi recebida, chamada de código da camada de domínio
para iniciar um novo jogo, e a resposta HTTP é retornada. A próxima etapa é considerar a refatoração.
Valerá a pena refatorar o teste para simplificar a escrita de novos testes, agrupando o código comum em
um só lugar:
WordzEndpointTest {
@Zombar
@BeforeEach
@Teste
var res
= httpClient.send(req,
HttpResponse.BodyHandlers.discarding());
afirmarIsso(res)
.hasStatusCode(HttpStatus.NO_CONTENT.code);
}
retornar HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/" + caminho));
Uma de nossas decisões de design é que um jogador não pode iniciar um jogo enquanto ele estiver em
andamento. Precisamos testar esse comportamento. Optamos por retornar um status HTTP de 409 Conflict para
indicar que um jogo já está em andamento para um jogador e um novo não pode ser iniciado para ele:
@Teste
.construir();
var res
= httpClient.send(req,
HttpResponse.BodyHandlers.discarding());
afirmarIsso(res)
.hasStatusCode(HttpStatus.CONFLICT.code);
}
2. Em seguida, execute o teste. Deve falhar, pois ainda não escrevemos o código de implementação:
3. Teste o código para informar que o jogo não pode ser reiniciado:
retornar
Resposta .of(HttpStatus.NO_CONTENT) .done();
}
Machine Translated by Google
resposta de retorno
.of(HttpStatus.CONFLITO)
.feito();
O teste é aprovado quando executado sozinho, agora que a implementação está em vigor. Vamos
executar todos os testes WordzEndpointTests para verificar nosso progresso.
As opções são parar o servidor web após cada teste ou iniciar o servidor web apenas uma vez para todos os testes.
Como se pretende que este seja um microsserviço de longa duração, começar apenas uma vez parece ser a melhor escolha aqui:
1. Adicione uma anotação @BeforeAll para iniciar o servidor HTTP apenas uma vez:
@Antes de tudo
void configuração() {
mockWordz = mock(Wordz.class);
Alteramos a anotação @BeforeEach para uma anotação @BeforeAll para fazer com que a criação do
endpoint aconteça apenas uma vez por teste. Para apoiar isso, também devemos criar o mock e usar
uma anotação no próprio teste para controlar o ciclo de vida dos objetos:
@ExtendWith(MockitoExtension.class)
2. Com todos os testes aprovados novamente, podemos considerar a refatoração do código. Uma melhoria
na legibilidade virá da extração de um método extractPlayer() . Também podemos tornar o código de
status HTTP condicional mais conciso:
HttpStatus.NO_CONTENT:
HttpStatus.CONFLITO;
resposta de retorno
.of(estado)
.feito(); }
}
}
lança IOException {
retornar novo Gson().fromJson(request.body(),
Jogador.class);
Concluímos agora a maior parte da codificação necessária para iniciar um jogo. Para lidar com a
condição de erro restante , agora podemos testar o código para retornar 400 BAD REQUEST se o
objeto Player não puder ser lido a partir da carga JSON. Omitiremos esse código aqui. Na próxima
seção, passaremos ao teste do código para adivinhar a palavra-alvo.
Jogando o jogo
Nesta seção, testaremos o código para jogar. Isso envolve o envio de várias tentativas de adivinhação ao endpoint até
que uma resposta de fim de jogo seja recebida.
Começamos criando um teste de integração para a nova rota /guess em nosso endpoint:
1. A primeira etapa é codificar a etapa de organização. Nosso modelo de domínio fornece o método
Assessment() na classe Wordz para avaliar a pontuação de um palpite, além de informar se o
jogo acabou. Para testar isso, configuramos o stub mockWordz para retornar um GuessResult válido
objeto quando o método avaliar() é chamado:
@Teste
void parcialmenteCorrectGuess() {
var pontuação = new Pontuação("-U---");
pontuação.assess("GUESS");
var resultado = new GuessResult(pontuação, falso, falso);
quando(mockWordz.assess(eq(player), eq("GUESS")))
.thenReturn(resultado);
2. A etapa Act chamará nosso endpoint com uma solicitação da web enviando a estimativa. Nossa decisão
de design é enviar uma solicitação HTTP POST para a rota /guess . O corpo da solicitação conterá
uma representação JSON da palavra adivinhada. Para criar isso, usaremos o registro GuessRequest
e use Gson para converter isso em JSON para nós:
@Teste
void parcialmenteCorrectGuess() {
Machine Translated by Google
.POST(ofString(corpo))
.construir();
pacote com.wordz.adapters.api;
importar com.wordz.domain.Player;
4. Em seguida, enviamos a solicitação via HTTP para nosso endpoint, aguardando a resposta:
@Teste
var res
= httpClient.send(req,
HttpResponse.BodyHandlers.ofString());
5. Em seguida, extraímos os dados corporais retornados e os afirmamos de acordo com nossas expectativas:
@Teste
pontuação.assess("GUESS");
quando(mockWordz.assess(eq(player), eq("GUESS")))
.thenReturn(resultado);
.POST(ofString(corpo)) .build();
var res
= httpClient.send(req,
HttpResponse.BodyHandlers.ofString());
resposta var
= novo Gson().fromJson(res.body(),
GuessHttpResponse.class);
.isEqualTo("PCXXX");
Asserções.assertThat(response.isGameOver())
Machine Translated by Google
.é falso();
Uma decisão de design de API aqui é retornar as pontuações por letra como um objeto String de
cinco caracteres. As letras únicas X, C e P são usadas para indicar letras incorretas, corretas e
parcialmente corretas . Capturamos essa decisão na afirmação.
6. Definimos um registro para representar a estrutura de dados JSON que retornaremos como resposta de
nosso ponto final:
pacote com.wordz.adapters.api;
7. Como decidimos fazer POST para uma nova rota /guess , precisamos adicionar esta rota à tabela de
roteamento. Também precisamos vinculá-lo a um método que executará a ação, que chamaremos de guessWord():
isto.palavraz = palavraz;
servidor = WebServer.create(host, porta);
tente
{ server.route(new Routes() {{ post("/start")
Adicionamos uma IllegalStateException para relançar quaisquer problemas que ocorram ao iniciar
o servidor HTTP. Para este aplicativo, essa exceção pode se propagar para cima e fazer com que
o aplicativo pare de ser executado. Sem um servidor web funcional, nenhum código web faz sentido
para correr.
Machine Translated by Google
9. Agora que temos os dados da solicitação , é hora de chamar nossa camada de domínio para fazer o
trabalho real. Capturaremos o objeto GuessResult retornado, para que possamos basear nossa
resposta HTTP do endpoint nele:
= palavraz.avaliar(gr.player(),
gr.adivinha());
retornar nulo;
} catch (IOException e) { lançar new
RuntimeException (e);
}
}
Machine Translated by Google
10. Optamos por retornar um formato de dados diferente do nosso endpoint em comparação com o
objeto GuessResult retornado do nosso modelo de domínio. Precisaremos transformar o resultado
do modelo de domínio:
GuessHttpResponse httpResponse
= novo
GuessHttpResponseMapper().from(resultado);
retornar novo Gson().toJson(httpResponse);
}
11. Adicionamos uma versão vazia do objeto que está fazendo a transformação, que é a classe
GuessHttpResponseMapper. Nesta primeira etapa, ele simplesmente retornará nulo:
pacote com.wordz.adapters.api;
importar com.wordz.domain.GuessResult;
}
}
Machine Translated by Google
13. Com um teste com falha implementado, agora podemos testar os detalhes da classe de transformação. Para
fazer isso, passamos a adicionar um novo teste de unidade chamado classe GuessHttpResponseMapperTest.
Observação
Os detalhes são omitidos, mas podem ser encontrados no GitHub – segue a abordagem padrão usada ao
longo do livro.
Como podemos ver na imagem anterior, o teste de integração passou! Hora de um merecido coffee break.
Bem, o meu é um bom chá inglês para o café da manhã, mas sou só eu. Depois disso, podemos testar a resposta a
quaisquer erros que ocorreram. Então é hora de reunir o microsserviço. Na próxima seção, montaremos nosso
aplicativo em um microsserviço em execução.
Integrando o aplicativo
Nesta seção, reuniremos os componentes de nosso aplicativo orientado a testes. Formaremos um microsserviço que
executa nosso endpoint e fornece a interface web frontend para nosso serviço. Ele usará o banco de dados Postgres
para armazenamento.
Machine Translated by Google
Precisamos escrever um método main() curto para vincular os principais componentes do nosso código.
Isso envolverá a criação de objetos concretos e a injeção de dependências em construtores. O método
main() existe na classe WordzApplication, que é o ponto de entrada para nosso serviço web totalmente integrado:
pacote com.wordz;
var palavraRepositório
= novo WordRepositoryPostgres(config.getDataSource());
waitUntilTerminated();
}
Machine Translated by Google
O método main() instancia o modelo de domínio e a dependência injeta nele a versão concreta
de nossas classes adaptadoras. Um detalhe notável é o método waitUntilTerminated(). Isso
evita que main() termine até que o aplicativo seja encerrado. Isso, por sua vez, mantém o
endpoint HTTP respondendo às solicitações.
Os dados de configuração do aplicativo são mantidos na classe WordzConfiguration. Ele possui configurações padrão
para o host do terminal e configurações de porta, juntamente com configurações de conexão de banco de dados. Eles
também podem ser transmitidos como argumentos de linha de comando. A classe e seu teste associado podem ser
vistos no código GitHub deste capítulo.
Na próxima seção, usaremos o aplicativo de serviço da web Wordz usando a popular ferramenta de teste
HTTP, Postman.
Usando o aplicativo
Para usar nosso aplicativo Web recém-montado, primeiro certifique-se de que as etapas de configuração do
banco de dados e a instalação do Postman descritas na seção Requisitos técnicos foram concluídas com
êxito. Em seguida, execute o método main() da classe WordzApplication no IntelliJ. Isso inicia o endpoint,
pronto para aceitar solicitações.
Assim que o serviço estiver em execução, a forma como interagimos com ele é enviando solicitações HTTP ao endpoint.
Inicie o Postman e (no macOS) uma janela semelhante a esta aparecerá:
Machine Translated by Google
Primeiro precisamos começar um jogo. Para fazer isso, precisamos enviar solicitações HTTP POST
para a rota /start em nosso endpoint. Por padrão, isso estará disponível em http://localhost:8080/start.
Precisamos enviar um corpo, contendo o texto JSON {"name":"testuser"} .
Podemos enviar esta solicitação do Postman. Clicamos no botão Criar uma solicitação na página inicial. Isso nos
leva a uma visualização onde podemos inserir a URL, selecionar o método POST e digitar nossos dados do corpo JSON:
Clique no botão azul Enviar . A captura de tela da Figura 15.11 mostra tanto a solicitação
enviada – na parte superior da tela – quanto a resposta. Neste caso, o jogo foi iniciado com
sucesso para o jogador chamado testuser. O endpoint funcionou conforme o esperado e
enviou um código de status HTTP 204 No Content. Isso pode ser visto no painel de resposta,
na parte inferior da captura de tela.
Uma rápida verificação do conteúdo da tabela de jogos no banco de dados mostra que uma linha para
este jogo foi criada:
palavrazdb=#
2. Agora podemos adivinhar a palavra pela primeira vez. Vamos tentar adivinhar "STARE". A solicitação
POST para isso e a resposta do nosso endpoint aparecem, conforme mostrado na captura de tela a seguir:
Resumo 309
O endpoint retorna um código de status HTTP de 200 OK. Desta vez, um corpo de dados
formatados em JSON é retornado. Vemos "scores":"PXPPC" indicando que a primeira letra do
nosso palpite, S, aparece em algum lugar da palavra, mas não na primeira posição. A segunda
letra do nosso palpite, T, está incorreta e não aparece na palavra alvo. Obtivemos mais duas letras
parcialmente corretas e uma letra final correta em nosso palpite, que era a letra E no final.
3. Faremos mais um palpite, trapaceando um pouco. Vamos enviar uma solicitação POST com um palpite
de "ARISE":
Ganhador! Vemos "pontuações":"CCCCC" informando que todas as cinco letras do nosso palpite
estão corretas. "isGameOver":true nos diz que nosso jogo terminou, nesta ocasião, com sucesso.
Resumo
Nesta seção, completamos nosso aplicativo Wordz. Usamos um teste de integração com TDD para
eliminar um endpoint HTTP para Wordz. Usamos bibliotecas HTTP de código aberto – Molecule, Gson e
Undertow. Fizemos uso eficaz da arquitetura hexagonal. Usando portas e adaptadores, essas estruturas
se tornaram um detalhe de implementação em vez de um recurso definidor do nosso design.
Machine Translated by Google
Montamos nosso aplicativo final para reunir a lógica de negócios mantida na camada de domínio com o
adaptador de banco de dados Postgres e o adaptador de endpoint HTTP. Trabalhando em conjunto, nosso
aplicativo forma um pequeno microsserviço.
Neste capítulo final, chegamos a um microsserviço típico, mas de pequena escala, que compreende uma API HTTP e um
banco de dados SQL. Desenvolvemos primeiro o teste de código, usando testes para orientar nossas escolhas de design.
Aplicamos os princípios SOLID para melhorar a forma como nosso software se encaixa. Aprendemos como as portas e
adaptadores de arquitetura hexagonal simplificam o design de código que funciona com sistemas externos. O uso da
arquitetura hexagonal é uma opção natural para o TDD, permitindo-nos desenvolver nossa lógica principal de aplicação
com os FIRST testes de unidade. Criamos primeiro um adaptador de banco de dados e um teste de adaptador HTTP,
usando testes de integração. Aplicamos os ritmos do TDD – Red, Green, Refactor e Arrange, Act and Assert ao nosso
trabalho. Aplicamos testes duplos usando a biblioteca Mockito para substituir sistemas externos, simplificando o
desenvolvimento.
Neste livro, cobrimos uma ampla gama de TDD e técnicas de design de software. Agora podemos criar código com menos
defeitos e com isso é mais seguro e fácil de trabalhar.
Perguntas e respostas
1. Que trabalho adicional poderia ser feito?
Trabalhos adicionais podem incluir a adição de um pipeline de Integração Contínua (CI) para que sempre
que confirmarmos o código, o aplicativo seja retirado do controle de origem, construído e todos os testes
executados. Poderíamos considerar a implantação e automação disso. Um exemplo pode ser empacotar o
aplicativo Wordz e o banco de dados Postgres como uma imagem Docker. Seria bom adicionar automação
de esquema de banco de dados, usando uma ferramenta como Flyway.
2. Poderíamos substituir a biblioteca Molecule e usar outra coisa para nosso endpoint web?
Sim. Como o endpoint da web fica em nossa camada adaptadora da arquitetura hexagonal, ele não
afeta a funcionalidade principal do modelo de domínio. Qualquer estrutura web adequada pode ser usada.
Leitura adicional
• https://martinfowler.com/articles/richardsonMaturityModel.html
Uma visão geral do que significa uma interface web REST, juntamente com algumas variações comuns
O livro do autor fornece mais detalhes sobre noções básicas de OO com alguns padrões de design úteis
• https://www.postman.com/
Uma ferramenta de teste popular que envia solicitações HTTP e exibe respostas
Machine Translated by Google
• http://molecule.vtence.com/
• https://undertow.io/
Um servidor HTTP para Java que funciona bem com a estrutura Molecule
• https://github.com/google/gson
• https://aws.amazon.com/what-is/restful-api/
• https://docs.oracle.com/en/java/javase/12/docs/api/java.net.
http/java/net/http/HttpClient.html
Documentação oficial Java sobre o cliente HHTP de teste usado neste capítulo
Machine Translated by Google
Machine Translated by Google
Índice
A B
abstração 19 código ruim
314 Índice
187 necessidade
de 189
Machine Translated by Google
Índice 315
design abstração de
avançando, com combinações sistema externo
de duas letras 85-92 160 requisitos de modelo de domínio
falhas de 160 testes duplos, substituição de
design revelando 166 sistemas externos, desafios 150
22, 23 testes, escrevendo benefícios 23, 24 transações acidentais 151
316 Índice
Link para
H
download do IntelliJ
criando 288
rotas, adicionando 289, 290 eu
Linguagem de marcação de hipertexto (HTML) 99
abstrações com
EU
vazamento
impedindo 25
ocultação de informações códigos legados 132
19 abordagem TDD de dentro para
fora 222 vantagens 223, gerenciando, sem testes, 39 bibliotecas adicionadas, ao projeto 284
224 desafios 224 Princípio de Substituição de Liskov (LSP)
uso 223 objetos trocáveis 109, 110 uso,
testes de integração 177-180 revisão em código de formas 111 ambiente
camada de adaptador 180, semelhante ao vivo 191 falhas
Índice 317
URL 133
318 Índice
S T
APIs sandbox 183 desempenho da equipe
teste de andaime 248 diminuindo 12, 13
riscos de segurança 208 dívida técnica 13
Link de limites de teste
referência de selênio definindo, com arquitetura hexagonal 226 testes
código de 186 formas duplos 122 benefícios
Índice 319
Estágio do ato
erros comuns, capturando 62, 63 definindo testes de aceitação do usuário 177, 184-186
59 código de
320 Índice
67-73
escrevendo 79-85
www.packtpub.com
Assine nossa biblioteca digital on-line para ter acesso total a mais de 7.000 livros e vídeos, bem como ferramentas
líderes do setor para ajudá-lo a planejar seu desenvolvimento pessoal e avançar em sua carreira. Para mais
informações, visite nosso site.
• Melhore seu aprendizado com Planos de Habilidades criados especialmente para você
Você sabia que a Packt oferece versões em e-book de todos os livros publicados, com arquivos PDF e ePub
disponíveis? Você pode atualizar para a versão do e-book em packtpub.com e, como cliente do livro
impresso, tem direito a um desconto na cópia do e-book. Entre em contato conosco em customercare@packtpub.
com para mais detalhes.
Em www.packtpub.com, você também pode ler uma coleção de artigos técnicos gratuitos, inscrever-se para receber uma
variedade de boletins informativos gratuitos e receber descontos e ofertas exclusivas em livros e e-books da Packt.
Machine Translated by Google
Se você gostou deste livro, pode estar interessado nestes outros livros de Packt:
Adam Tibi
ISBN: 978-1-80323-019-1
• Usando o estilo TDD para testes unitários em conjunto com DDD e melhores práticas
ISBN: 978-1-80324-200-2
324
Agora que você concluiu o Desenvolvimento Orientado a Testes com Java, adoraríamos ouvir sua opinião! Se
você comprou o livro na Amazon, clique aqui para ir direto para a página de resenhas deste livro na Amazon e
compartilhar seus comentários ou deixar uma resenha no site onde você o comprou.
Sua avaliação é importante para nós e para a comunidade de tecnologia e nos ajudará a garantir que estamos entregando
conteúdo de excelente qualidade.
Machine Translated by Google
325
Você gosta de ler em qualquer lugar, mas não consegue levar seus livros impressos para qualquer lugar?
A compra do seu e-book não é compatível com o dispositivo de sua escolha?
Não se preocupe, agora com cada livro Packt você obtém uma versão em PDF sem DRM desse livro, sem nenhum custo.
Leia em qualquer lugar, em qualquer lugar, em qualquer dispositivo. Pesquise, copie e cole códigos de seus livros técnicos
favoritos diretamente em seu aplicativo.
As vantagens não param por aí, você pode obter acesso exclusivo a descontos, newsletters e ótimo conteúdo gratuito em sua
caixa de entrada diariamente
https://packt.link/free-ebook/978-1-80323-623-0
3. É isso! Enviaremos seu PDF grátis e outros benefícios diretamente para seu e-mail