Você está na página 1de 348

Machine Translated by Google

Machine Translated by Google

Desenvolvimento orientado a testes com Java

Crie software de alta qualidade escrevendo testes primeiro com


arquitetura SOLID e hexagonal

Alan Mellor

BIRMINGHAM-MUMBAI
Machine Translated by Google

Desenvolvimento orientado a testes com Java


Direitos autorais © 2022 Packt Publishing

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.

Gerente de Produto do Grupo: Gebin George


Gerente de produto editorial: Arvind Sharma
Editora Sênior: Nisha Cleetus

Editor Técnico: Jubit Pincy

Editor de texto: Edição Safis

Coordenador do Projeto: Manisha Singh

Revisor: Safis Editing


Indexador: Subalakshmi Govindhan

Designer de Produção: Shankar Kalbhor


Executivo de Desenvolvimento de Negócios: Kriti Sharma

Coordenador de Marketing: Sonakshi Bubbar

Publicado pela primeira vez: janeiro de 2023

Referência de produção: 1231222

Publicado por Packt Publishing Ltd.

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

Parte 1: Como chegamos ao TDD

1
Construindo o Caso para TDD 3

Escrevendo código mal 3 Acoplamento e coesão 10

5
Entendendo por que código incorreto é escrito
Diminuindo o desempenho da equipe 12

Reconhecendo código incorreto 6 Diminuição dos resultados de negócios 14


Nomes de variáveis ruins 6
Resumo 16
Função, método e nomes de classe incorretos 6 16
Perguntas e respostas
Construções propensas a erros 9
Leitura adicional 16

2
Usando TDD para criar um bom código 17

Projetando código de boa qualidade 17 Prevenindo falhas lógicas 25

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

31 Gerenciando suas expectativas em relação ao TDD 37


Escrever testes me deixa mais lento
Compreender os benefícios de desacelerar 32 37
Nosso código é muito complexo para testar
Superar objeções aos testes que 37
Compreendendo as causas do código não testável
nos atrasam 33
Reformulando a relação entre bom design e
34 testes simples 38
Os testes não podem prevenir todos os bugs
Gerenciando código legado sem testes 39
Entendendo por que as pessoas dizem que os testes não

conseguem detectar todos os bugs 34


Não sei o que testar até escrever o código
Superando objeções para não detectar
39
todos os bugs 35
Compreender a dificuldade de começar com testes
40
Como você sabe que os testes estão certos? 35
Compreendendo as preocupações por trás da escrita Superando a necessidade de escrever primeiro o
de testes quebrados 35 código de produção 40

Fornecendo garantia de que testamos nossos testes 36


Resumo 41

TDD garante um bom código 36 Perguntas e respostas 41


Compreendendo as expectativas infladas pelo problema 36
Leitura adicional 41

Parte 2: Técnicas TDD


4
Construindo um aplicativo usando TDD 45

Requerimentos técnicos 45 Lendo histórias de usuários – a base do


planejamento 51
Preparando nosso ambiente de desenvolvimento 46
Combinando desenvolvimento ágil com TDD 52
Instalando o IDE IntelliJ 46

Configurando o projeto Java e as bibliotecas 46 53


Resumo
Perguntas e respostas 53
Apresentando o aplicativo Wordz 48
Descrevendo as regras do Wordz 49 Leitura adicional 54

Explorando métodos ágeis 50


Machine Translated by Google

Índice ix

5
Escrevendo nosso primeiro teste 55
Requerimentos técnicos 55 Preservando o encapsulamento 64

Iniciando TDD: Arrange-Act-Assert 56 65


Aprendendo com nossos testes
Definindo a estrutura de teste 56 65
Uma etapa de organização confusa

Trabalhando de trás para frente a partir dos resultados 58 66


Uma etapa confusa do ato

Aumentando a eficiência do fluxo de trabalho 59 66


Uma etapa de afirmação confusa

59 Limitações dos testes unitários 66


Definindo um bom teste
60 Cobertura de código – uma métrica muitas vezes sem sentido 67
Aplicando os PRIMEIROS princípios
Escrevendo os testes errados 67
Usando uma afirmação por teste 61

Decidindo sobre o escopo de um teste de unidade 61 Começando Wordz 67

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

Seguindo o ciclo RGR 75 Resumo 92

Começando no vermelho 76 Perguntas e respostas 92


Mantenha a simplicidade – mudando para o verde 77 93
Leitura adicional
Refatorando para limpar código 78

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

100 DIP – ocultando detalhes irrelevantes 104


Capacidade de reutilizar código
Machine Translated by Google

x Índice

Aplicando DI ao código de formas 106 ISP – interfaces eficazes 115

Revendo o uso do ISP no código de formas 115


LSP – objetos trocáveis 109

Revendo o uso de LSP no código de formas 111 Resumo 117

112 Perguntas e respostas 117


OCP – design extensível
Adicionando um novo tipo de forma 114

Duplas de teste – Stubs e simulações 119

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

O objetivo do teste duplica 122 Desfocando a distinção entre stubs e mocks


124 142
Fazendo a versão de produção do código
Correspondentes de argumentos – personalizando o comportamento
Usando stubs para resultados pré-preparados 126 de duplicatas de teste 142
Quando usar objetos stub 127
Conduzindo código de tratamento
Usando simulações para verificar interações 127 de erros com testes 144

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

Arquitetura Hexagonal – Desacoplamento de Sistemas Externos 149

Requerimentos técnicos 150 Acionar acidentalmente transações reais de


testes 151
Por que os sistemas externos são difíceis 150
Que dados devemos esperar? 152
Problemas ambientais trazem problemas 151
Chamadas do sistema operacional e hora do sistema 152
Machine Translated by Google

Índice XI

Desafios com serviços de terceiros 152 modelo de domínio 165

Decidindo sobre uma abordagem de programação 165


Inversão de dependência para o resgate 153
Generalizando esta abordagem para a arquitetura Substituindo duplicatas de teste
hexagonal 154 por sistemas externos 166

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

adaptadores Por que o formato hexagonal? 160


Wordz – abstraindo o banco de dados 168

Abstraindo o sistema externo 160 Projetando a interface do repositório 168

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

Usando bibliotecas e frameworks no Leitura adicional 174

10
PRIMEIROS Testes e a Pirâmide de Testes 175

Requerimentos técnicos 175 Por que precisamos de integração contínua? 187

176 Por que precisamos de entrega contínua? 189


A pirâmide de teste
Testes unitários – PRIMEIROS testes 178 Entrega contínua ou implantação contínua?
190
Testes de integração O 179
Pipelines práticos de CI/CD 190

que um teste de integração deve abranger? 180 Ambientes de teste 191

Testando adaptadores de banco 181 192


Teste em produção
de dados Testando serviços 182

183 Wordz – teste de integração para nosso


da Web Testes de contratos orientados ao consumidor
banco de dados 194
Testes ponta a ponta e de aceitação do usuário 184 Buscando uma palavra do banco de dados 194

Ferramentas de teste de aceitação 186


Resumo 197
Pipelines CI/CD e ambientes Perguntas e respostas 197
de teste 187
Leitura adicional 198
O que é um pipeline de CI/CD? 187
Machine Translated by Google

xii Índice

11
Explorando TDD com Garantia de Qualidade 199

Testando a interface do usuário 205


TDD – seu lugar no panorama geral da qualidade
Entendendo 199 Avaliando a experiência do usuário 207

os limites do TDD Não há mais 200


Testes de segurança e monitoramento
necessidade de testes manuais? 200 208
de operações

Exploratório manual – descobrindo o Incorporando elementos manuais em


inesperado 201 Fluxos de trabalho de CI/CD 209

Revisão de código e programação Resumo 210

em conjunto 203 211


Perguntas e respostas
Teste de interface do usuário e experiência Leitura adicional 211
do usuário 205

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

Outside-in funciona bem com adaptadores 227


Sempre podemos testar mais tarde, certo? 218
As histórias de usuários podem ser testadas em todo o
Testar mais tarde é mais fácil para um iniciante em 219
modelo de domínio 228
TDD Testar mais tarde torna mais difícil testar todos os

caminhos de código 219 Resumo 230


Testar mais tarde torna mais difícil influenciar o Perguntas e respostas 230
Design de software 220
Leitura adicional 231
Teste mais tarde pode nunca acontecer 221
Machine Translated by Google

Índice xiii

Parte 3: TDD do mundo real

13
Conduzindo a camada de domínio 235

Requerimentos técnicos 235 Respondendo a um palpite correto 255

Iniciando um novo jogo 235 Triangulação do jogo devido a muitas suposições


incorretas 256
Test-drive iniciando um novo jogo 236
Triangulação da resposta para
Acompanhando o progresso do jogo 238
adivinhar após o fim do jogo 257
Triangulação de seleção de palavras 244
Revendo nosso design 260

Jogando o jogo 249


Resumo 263
Projetando a interface de pontuação 250
Perguntas e respostas 264
Triangulação do acompanhamento do progresso do jogo 252
Leitura adicional 264
Terminando o jogo 254

14
Conduzindo a camada de banco de dados 267

Requerimentos técnicos 267 Implementando o adaptador


267 WordRepository 276
Instalando o banco de dados Postgres
Acessando o banco de dados 277
Criando um teste de integração de banco de dados 268
Implementando GameRepository 279
Criando um teste de banco de dados com DBRider 269

272 Resumo 280


Expulsando o código de produção
Perguntas e respostas 280

Leitura adicional 281

15
Conduzindo a camada da Web 283

Requerimentos técnicos 283 Criando nosso servidor HTTP 288

Iniciando um novo jogo 284 Adicionando rotas ao servidor HTTP 289

Conectando-se à camada de domínio 290


Adicionando bibliotecas necessárias ao projeto 284
Refatorando o código inicial do jogo 293
Escrevendo o teste com falha 285
Machine Translated by Google

XIV Índice

Lidando com erros ao iniciar um jogo 294 Usando o aplicativo 306


Corrigindo testes com falha inesperada 296 309
Resumo
Jogando o jogo 298 Perguntas e respostas 310

Integrando o aplicativo 304 Leitura adicional 310

Índice 313

Outros livros que você pode gostar 322


Machine Translated by Google

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.

O desenvolvimento orientado a testes (TDD), os princípios SOLID e a arquitetura hexagonal permitem


que os desenvolvedores projetem código que funciona e é fácil de trabalhar. O desenvolvimento se
concentra nos fundamentos da engenharia de software. Essas práticas são a base técnica por trás de
uma base de código fácil e segura de alterar e sempre pronta para ser implantada.

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.

Para quem é este livro

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 que este livro cobre

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.

Para aproveitar ao máximo este livro


Este livro pressupõe que você conheça Java moderno básico e possa escrever programas curtos usando classes,
expressões lambda JDK 8 e usar a palavra-chave var JDK 11 . Também pressupõe que você pode usar o git básico
comandos, instale software de downloads da web em seu computador e tenha familiaridade básica
com o IntelliJ IDEA Java IDE. O conhecimento básico de SQL, HTTP e REST será útil nos capítulos finais.

Software/hardware abordado no livro Requisitos do sistema operacional


Amazon Corretto JDK 17 LTS Windows, macOS ou Linux

IntelliJ IDEA 2022.1.3 Community Edition Windows, macOS ou Linux


JUnit 5 Windows, macOS ou Linux

AfirmarJ Windows, macOS ou Linux


Mockito Windows, macOS ou Linux
DBRider Windows, macOS ou Linux

Postgres Windows, macOS ou Linux

psql Windows, macOS ou Linux


Molécula Windows, macOS ou Linux

idiota Windows, macOS ou Linux

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

Baixe os arquivos de código de exemplo

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!

Baixe as imagens coloridas

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

Há uma série de convenções de texto usadas ao longo deste livro.

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.”

Um bloco de código é definido da seguinte forma:

classe pública DiceRoll {

final privado int NUMBER_OF_SIDES = 6;


final privado RandomGenerator rnd =
RandomGenerator.getDefault();

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:

classe pública DiceRoll {

final privado int NUMBER_OF_SIDES = 6;


final privado RandomGenerator rnd =
RandomGenerator.getDefault();
Machine Translated by Google

Prefácio XIX

Qualquer entrada ou saída de linha de comando é escrita da seguinte forma:

int final privado NUMBER_OF_SIDES = 6

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”.

Dicas ou notas importantes

Apareça assim.

Entrar em contato

O feedback dos nossos leitores é sempre bem-vindo.

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

Compartilhe seus pensamentos

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

Baixe uma cópia gratuita em PDF deste livro


Obrigado por adquirir este livro!

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

Siga estas etapas simples para obter os benefícios:

1. Digitalize o código QR ou visite o link abaixo

https://packt.link/free-ebook/978-1-80323-623-0

2. Envie seu comprovante de compra

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.

Esta parte possui os seguintes capítulos:

• Capítulo 1, Construindo o Caso para TDD

• Capítulo 2, Usando TDD para criar um bom código

• Capítulo 3, Dissipando Mitos Comuns sobre TDD


Machine Translated by Google
Machine Translated by Google

Construindo o Caso para TDD

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.

Neste capítulo, vamos cobrir os seguintes tópicos principais:

• Escrever código mal


• Reconhecendo código incorreto

• Diminuição do desempenho da equipe

• Diminuição dos resultados de negócios

Escrevendo código mal


Como todo desenvolvedor sabe, parece muito mais fácil escrever código ruim do que projetar código bom.
Podemos definir um bom código como sendo fácil de entender e seguro para alterar. Código ruim é,
portanto, o oposto disso, onde é muito difícil ler o código e entender qual problema ele deveria resolver.
Tememos alterar códigos incorretos – sabemos que provavelmente quebraremos alguma coisa.

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

4 Construindo o Caso para TDD

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:

Figura 1.1 – Listagem de códigos da imobiliária


Machine Translated by Google

Escrevendo código mal 5

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.

Entendendo por que código incorreto é escrito

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:

• Falta de tempo para refinar o código devido aos prazos do projeto

• 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

• Desconhecimento da área de assunto do código

• Falta de familiaridade com os idiomas locais e estilos de desenvolvimento

• Usar expressões idiomáticas de uma linguagem de programação diferente de forma inadequada

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

6 Construindo o Caso para TDD

Reconhecendo código incorreto

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.

Nomes de variáveis ruins

Um bom código é autodescritivo e seguro para alteração. Código ruim não é.

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.

Prática recomendada para nomear variáveis

Descreva os dados contidos, não o tipo de dados.

Agora temos uma ideia melhor de como nomear variáveis. Agora, vamos ver como nomear funções, métodos e classes
corretamente.

Função, método e nomes de classe incorretos

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

Reconhecendo código incorreto 7

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.

Melhores práticas para nomes de métodos e funções

Descreva o resultado, não a implementação.

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.

O comprimento de um nome depende do namespace

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:

• A variável nomeada tem um escopo pequeno de apenas algumas linhas

• O próprio nome da classe fornece a maior parte da descrição

• O nome existe em algum outro namespace, como um nome de classe

Vejamos um exemplo de código para cada caso para deixar isso claro.
Machine Translated by Google

8 Construindo o Caso para TDD

O código a seguir calcula o total de uma lista de valores, usando um nome curto de variável, total:

int calculaTotal(Lista<Integer> valores) {


total interno = 0;

for (Inteiro v: valores) {


total += v;
}

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.

No método a seguir, temos um parâmetro com o nome abreviado gc:

private void draw(GraphicsContext gc) {


// código usando gc omitido
}

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():

classe pública ProfileImage {


public void draw(WebResponse wr) {
//Código omitido
}
}

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

Reconhecendo código incorreto 9

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.

Construções propensas a erros

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:

int calculaTotal(Lista<Integer> valores) {


total interno = 0;

for (int i=0; i<valores.tamanho(); i++) {


total += valores.get(i);
}

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

• Certificando-se de que nosso índice de loop i seja inicializado em 0

• Garantir que usamos < e não <= ou == em nossa comparação de loop

• Garantir que incrementamos o índice do i loop em exatamente um

• Garantir que adicionamos o valor do índice atual na lista ao total

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

10 Construindo o Caso para TDD

É 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:

int calculaTotal(Lista<Integer> valores) {


retornar valores.stream().mapToInt(v -> v).sum();

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.

Baixa coesão dentro de uma classe

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

Reconhecendo código incorreto 11

Figura 1.2 – Baixa coesão

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:

• Mudanças na tecnologia de banco de dados

• Mudanças no layout de visualização da web

• Mudanças na tecnologia do mecanismo de modelo da web

• Mudanças na tecnologia do mecanismo de modelo de e-mail

• Mudanças no algoritmo de geração de feed de notícias

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

12 Construindo o Caso para TDD

Alto acoplamento entre classes

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:

Figura 1.3 – Acoplamento alto

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.

Diminuindo o desempenho da equipe


Uma boa maneira de analisar códigos ruins é aquele que não possui práticas técnicas que ajudem outros desenvolvedores
a entender o que estão fazendo.
Machine Translated by Google

Diminuindo o desempenho da equipe 13

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.

Os dois estudos a seguir são interessantes nesse sentido:

• 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

14 Construindo o Caso para TDD

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.

Diminuição dos resultados de negócios


Não é apenas a equipe de desenvolvimento que sofre os efeitos de códigos incorretos. É ruim para todo o negócio.

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.

A vantagem de encontrar falhas antecipadamente é mostrada no diagrama a seguir:


Machine Translated by Google

Diminuição dos resultados de negócios 15

Figura 1.4 – Custos de descoberta de defeitos

Na figura anterior, o custo do reparo de um defeito aumenta quanto mais tarde ele for encontrado:

• Encontrado por um teste com falha antes do código:

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

problema em nosso código.

• Encontrado por um teste com falha após o código:

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

antes de descobrir o defeito.

• Encontrado durante o controle de qualidade manual:

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.

• Encontrado pelo usuário final quando o código está em produção:

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

16 Construindo o Caso para TDD

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.

3. É mais fácil escrever código bom ou código ruim?

É 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.

Neste capítulo, vamos cobrir os seguintes tópicos principais:

• Projetando código de boa qualidade

• Revelando falhas de design

• Prevenção de falhas lógicas

• Proteção contra defeitos futuros

• Documentando nosso código

Projetando código de boa qualidade


Código de boa qualidade não acontece por acidente. É intencional. É o resultado de milhares de pequenas decisões, cada uma delas
moldando a facilidade de leitura, teste, composição e alteração do nosso código. Devemos escolher entre hacks rápidos e sujos,
onde não temos ideia de quais casos extremos são abordados, e abordagens mais robustas, onde estamos confiantes de que não
importa como o usuário faça mau uso do nosso código, ele funcionará conforme o esperado.

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

18 Usando TDD para criar um bom código

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.

Então, o que é um bom código? O que pretendemos?

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

• Cuide dos detalhes em particular

• Evite complexidade acidental

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 ???.

Aqui está um exemplo rápido:

booleano público ??? (int???) {


se (???> ???) {
retornar ???;

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

Projetando código de boa qualidade 19

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?

• Variável – Diga o que ela contém. Por que eu acessaria 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.

Cuide dos detalhes em particular


Cuidar dos detalhes em particular é uma maneira simples de descrever os conceitos de abstração e ocultação
de informações da ciência da computação. Estas são ideias fundamentais que nos permitem quebrar sistemas
complexos em partes menores e mais simples.

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

20 Usando TDD para criar um bom código

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.

A abstração acontece em todos os lugares em um bom software.

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.

Evite complexidade acidental


Este é meu destruidor favorito de bons códigos – códigos complexos que simplesmente nunca precisaram existir.

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

Projetando código de boa qualidade 21

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:

public boolean isTrue (Boolean b) {


resultado booleano = falso;

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

22 Usando TDD para criar um bom código

Um equivalente melhor seria o seguinte:

public boolean isTrue (Boolean b) {


retornar Boolean.TRUE.equals(b);
}

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.

Revelando falhas de design


Um design ruim é realmente ruim. É a causa raiz do software ser difícil de mudar e de trabalhar. Você nunca
pode ter certeza se suas alterações funcionarão porque nunca pode ter certeza do que um design ruim
realmente está fazendo. Alterar esse tipo de código é assustador e muitas vezes adiado. Seções inteiras de
código podem ser deixadas para apodrecer com apenas um /* Aqui estão os dragões! */ comentário para mostrar isso.

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

Revelando falhas de design 23

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.

Podemos fazer perguntas como as seguintes:

• É fácil de configurar?

• É fácil pedir-lhe para fazer alguma coisa?

• O resultado é fácil de trabalhar?

• É difícil usá-lo da maneira errada?

• Fizemos alguma suposição incorreta sobre isso?

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.

Analisando os benefícios de escrever testes antes do código de produção

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

24 Usando TDD para criar um bom código

Esta é uma grande melhoria, mas ainda não é o padrão ouro, pois leva a alguns problemas sutis:

• Testes ausentes

• Abstrações com vazamento

Testes ausentes – erros não detectados

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.

Suponha que eu acabe escrevendo algum código como este:

public boolean isAllowed18PlusProducts (Idade inteira) {


return (idade! = nulo) && idade.intValue() > 18;
}

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

Prevenindo falhas lógicas 25

Abstrações vazadas – expondo detalhes irrelevantes

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.

Poderíamos simplesmente adicionar um parâmetro Connection para corrigir isso:

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.

Prevenindo falhas lógicas


A ideia de erros lógicos talvez seja o que todos pensam primeiro quando falamos em testes: funcionou certo?
Machine Translated by Google

26 Usando TDD para criar um bom código

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.

Compreender os limites dos testes manuais

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.

Resolvendo problemas automatizando os testes

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

Proteção contra defeitos futuros 27

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.

Proteção contra defeitos futuros


À medida que aumentamos nosso código escrevendo testes primeiro, poderíamos simplesmente excluir cada teste depois que ele fosse aprovado.

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

28 Usando TDD para criar um bom código

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.

Documentando nosso código


Todo mundo gosta de documentação útil e clara, mas não quando ela está desatualizada e não tem relação com a base de
código atual.

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.

2. Os testes podem substituir a documentaçã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.

3. Quais são os problemas em escrever código de produção antes dos testes?

Se escrevermos o código de produção primeiro e depois adicionarmos testes, é mais provável que enfrentemos os seguintes
problemas:

Faltando casos extremos quebrados em condicionais

Vazamento de detalhes de implementação por meio de interfaces

Esquecendo testes importantes

Tendo caminhos de execução não testados

Criando código difícil de usar

Forçar mais retrabalho quando falhas de projeto são reveladas posteriormente no processo
Machine Translated by Google

30 Usando TDD para criar um bom código

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.

Neste capítulo, vamos cobrir os seguintes mitos:

• “Escrever testes me deixa lento”

• “Os testes não podem prevenir todos os bugs”

• “Como você sabe que os testes estão corretos”

• “TDD garante um bom código”

• “Nosso código é muito complexo para ser testado”

• “Não sei o que testar até escrever o código”

Escrever testes me deixa mais lento


Escrever testes que retardam o desenvolvimento é uma reclamação popular sobre o TDD. Esta crítica tem
algum mérito. Pessoalmente, sempre senti que o TDD me tornou mais rápido, mas a pesquisa acadêmica discorda.
Uma meta-análise de 18 estudos primários realizada pela Association for Computing Machinery mostrou que o TDD melhorou a
produtividade em ambientes acadêmicos, mas acrescentou tempo extra em contextos industriais. No entanto, essa não é a história
completa.
Machine Translated by Google

32 Dissipando mitos comuns sobre TDD

Compreender os benefícios de desacelerar

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.

Podemos ver a diferença na quantidade de trabalho a ser feito nesta figura:

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

Escrever testes me deixa lento 33

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.

Superar objeções aos testes que nos atrasam


Crie um caso que rastreie o tempo gasto em defeitos não descobertos em controle de qualidade manual e implantações com falha.
Encontre alguns números aproximados do tempo necessário para que o problema ativo mais recente seja corrigido.
Descubra qual teste de unidade ausente poderia ter evitado isso. Agora calcule quanto tempo levaria para escrever.
Apresente esses números às partes interessadas. Pode ser ainda mais eficaz calcular o custo de todo esse tempo de
engenharia e qualquer perda de receita.

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

34 Dissipando mitos comuns sobre TDD

Os testes não podem prevenir todos os bugs

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 você não pensou em escrever

• Defeitos que surgem devido a interações no nível do sistema

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

Como você sabe que os testes estão certos? 35

Superando objeções para não detectar todos os bugs

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?

Como você sabe que os testes estão certos?


Esta é uma objeção que tem mérito, por isso precisamos compreender profundamente a lógica por trás dela. Esta é
uma objeção comum de pessoas não familiarizadas com a escrita de testes automatizados, pois não entendem como
evitamos testes incorretos. Ao ajudá-los a ver as salvaguardas que implementamos, podemos ajudá-los a reformular
o seu pensamento.

Compreendendo as preocupações por trás da escrita de testes quebrados

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

36 Dissipando mitos comuns sobre TDD

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.

Fornecendo garantia de que testamos nossos testes


A essência desse argumento é que códigos curtos e simples podem ser inspecionados visualmente. Para garantir isso, mantemos
a maioria dos nossos testes unitários simples e curtos o suficiente para serem raciocinados. Quando os testes ficam muito
complexos, extraímos essa complexidade para sua própria unidade de código. Desenvolvemos isso usando TDD e acabamos
tornando o código de teste original simples o suficiente para ser inspecionado e o utilitário de teste simples o suficiente para que
seus testes sejam inspecionados, um exemplo clássico de dividir e conquistar.

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.

TDD garante um bom código


Assim como muitas vezes há objeções excessivamente pessimistas ao TDD, aqui está uma visão oposta: o TDD garante
bom código. Como o TDD é um processo e pretende melhorar o código, é bastante razoável supor que usar o TDD é tudo o que
você precisa para garantir um bom código. Infelizmente, isso não é de todo correto. O TDD ajuda os desenvolvedores a escrever
um bom código e ajuda como feedback para nos mostrar onde cometemos erros de design e lógica. No entanto, não pode garantir
um bom código.

Compreendendo as expectativas infladas pelo problema


A questão aqui é um mal-entendido. TDD não é um conjunto de técnicas que afetam diretamente suas decisões de design. É um
conjunto de técnicas que ajudam a especificar o que você espera que um trecho de código faça, quando, sob quais condições e
dado um design específico. Isso deixa você livre para escolher o design, o que espera que ele faça e como implementar esse
código.
Machine Translated by Google

Nosso código é muito complexo para testar 37

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.

Gerenciando suas expectativas em relação ao TDD

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

usado. Essa é uma grande vantagem.

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?

Nosso código é muito complexo para testar

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?

Compreendendo as causas do código não testável

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

• O código está tão emaranhado que não o entendemos mais


Machine Translated by Google

38 Dissipando mitos comuns sobre TDD

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.

Porém, como podemos persuadir nossas equipes com essas ideias?

Reformulando a relação entre bom design e testes simples

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

Não sei o que testar até escrever o código 39

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.

Gerenciando código legado sem testes


Encontraremos código legado sem testes que precisamos manter. Freqüentemente, esse código tornou-se bastante
incontrolável e, idealmente, precisa ser substituído, exceto que ninguém sabe mais o que ele faz.
Pode não haver documentação ou especificação escrita para nos ajudar a entendê-lo. Qualquer material escrito que exista
pode estar completamente desatualizado e inútil. Os autores originais do código podem ter mudado para uma equipe ou
empresa diferente.

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:

1. Execute o código legado, fornecendo-lhe todas as combinações possíveis de entradas.

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.

Não sei o que testar até escrever o código

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

40 Dissipando mitos comuns sobre TDD

Compreender a dificuldade de começar com testes

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.

Superando a necessidade de escrever primeiro o código de produção

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.

2. O TDD elimina as contribuições do design humano?

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.

3. Por que minha equipe de projeto não usa TDD?

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.

Esta parte possui os seguintes capítulos:

• Capítulo 4, Construindo uma aplicação usando TDD

• Capítulo 5, Escrevendo nosso primeiro teste

• Capítulo 6, Seguindo os Ritmos do TDD

• Capítulo 7, Design de Condução – TDD e SOLID

• Capítulo 8, Duplas de Teste – Stubs e Mocks

• Capítulo 9, Arquitetura Hexagonal – Desacoplando Sistemas Externos

• Capítulo 10, PRIMEIROS testes e a pirâmide de testes

• Capítulo 11, Como o TDD se encaixa na garantia de qualidade

• Capítulo 12, Teste primeiro, teste depois, teste nunca


Machine Translated by Google
Machine Translated by Google

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á.

Neste capítulo, abordaremos os seguintes tópicos:

• Apresentando o aplicativo Wordz

• Explorando métodos ágeis

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

46 Construindo um aplicativo usando TDD

Preparando nosso ambiente de desenvolvimento

Para este projeto, usaremos as seguintes ferramentas:

• IntelliJ IDEA IDE 2022.1.3 (Community Edition) ou superior

• Amazon Corretto Java 17 JDK

• A estrutura de teste unitário JUnit 5

• A estrutura de asserções fluentes AssertJ

• O sistema de gerenciamento de dependências Gradle

Começaremos instalando nosso IDE Java, o JetBrains IntelliJ IDE Community Edition, antes de adicionar o restante
das ferramentas.

Instalando o IDE IntelliJ

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.

Para instalar o IntelliJ, consulte as seguintes etapas:

1. Acesse https://www.jetbrains.com/idea/download/.

2. Clique na guia do seu sistema operacional.

3. Role para baixo até a seção Comunidade.

4. Siga as instruções de instalação do seu sistema operacional.

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.

Configurando o projeto Java e as bibliotecas

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

Para fazer isso, veja as seguintes etapas:

1. Em seu navegador, acesse https://github.com/PacktPublishing/Test Driven-Development-with-Java.

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:

clone git https://github.com/PacktPublishing/Test-Driven Development-with-Java.git

3. Inicie o IntelliJ. Você deverá ver a tela de boas-vindas:

Figura 4.1 – Tela de boas-vindas do IntelliJ

4. Clique em Abrir e navegue até a pasta Chapter04 do repositório que acabamos de clonar.
Clique para destacá-lo:

Figura 4.2 – Selecione a pasta de código

5. Clique no botão Abrir .


Machine Translated by Google

48 Construindo um aplicativo usando TDD

6. Aguarde o IntelliJ importar os arquivos. Você deverá ver este espaço de trabalho aberto:

Figura 4.3 – Visualização do espaço de trabalho do IntelliJ

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.

Apresentando o aplicativo Wordz


Nesta seção, descreveremos em alto nível o aplicativo que iremos construir, antes de passarmos a examinar o
processo ágil que usaremos para construí-lo. O aplicativo se chama Wordz e é baseado em um popular jogo de
adivinhação de palavras. Os jogadores tentam adivinhar uma palavra de cinco letras. Os pontos são marcados com
base na rapidez com que o jogador adivinha a palavra. O jogador recebe feedback sobre cada palpite para orientá-lo
na resposta certa. Construiremos os componentes do lado do servidor desta aplicação no restante deste livro usando
diversas técnicas de TDD.
Machine Translated by Google

Apresentando o aplicativo Wordz 49

Descrevendo as regras do Wordz


Para jogar Wordz, o jogador terá até seis tentativas para adivinhar uma palavra de cinco letras. Após cada tentativa, as
letras da palavra são destacadas da seguinte forma:

• A letra correta na posição correta tem um fundo preto

• A letra correta na posição errada tem um fundo cinza

• Letras incorretas não presentes na palavra têm fundo branco

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:

Figura 4.4 – O jogo Wordz

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

50 Construindo um aplicativo usando TDD

Explorando métodos ágeis


À medida que construímos o Wordz, usaremos uma abordagem iterativa, onde construímos o aplicativo como uma série de
recursos com os quais nossos usuários podem trabalhar. Isso é conhecido como desenvolvimento ágil. É eficaz porque nos
permite enviar recursos aos usuários com antecedência e em uma programação regular. Isso nos permite, como
desenvolvedores, aprender mais sobre os problemas que estamos resolvendo e como fica um bom design de software à medida
que avançamos. Esta seção comparará os benefícios do desenvolvimento ágil com as abordagens em cascata e, em seguida,
apresentará uma ferramenta ágil de coleta de requisitos chamada histórias de usuários.

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.

Em um projeto em cascata, dividimos o desenvolvimento em etapas sequenciais:

1. Coleta de requisitos

2. Realizando uma análise de requisitos

3. Criação de um design de software completo

4. Escrevendo todo o código

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

Explorando métodos ágeis 51

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.

Lendo histórias de usuários – a base do planejamento


Como o desenvolvimento é iterativo e abrange refatoração e retrabalho, faz sentido que os métodos antigos de
especificação de requisitos não funcionem. Não somos mais atendidos por milhares de páginas de requisitos definidos
antecipadamente. Seremos melhor atendidos se considerarmos um requisito de cada vez, construí -lo e aprendermos
com ele. Com o tempo, podemos priorizar os recursos que os usuários desejam e aprender mais sobre a aparência de
um bom design.

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:

• Os requisitos são apresentados um de cada vez de forma isolada

• Enfatizamos o valor para o usuário, não o impacto técnico no sistema

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:

Figura 4.5 – A história do usuário


Machine Translated by Google

52 Construindo um aplicativo usando TDD

O formato de uma história de usuário é sempre o mesmo – compreende três seções:

• Como [pessoa ou máquina que usa o software],…

• Quero [um resultado específico desse software]…

• … para que [uma tarefa que é importante seja alcançada].

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.

Combinando desenvolvimento ágil com TDD

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

O fluxo de trabalho que usaremos é típico de um projeto ágil de TDD:

1. Escolha uma história de usuário priorizada em termos de impacto.

2. Pense um pouco sobre o design a ser almejado.

3. Use TDD para escrever a lógica do aplicativo no núcleo.

4. Use TDD para escrever código para conectar o núcleo a um banco de dados.

5. Use TDD para escrever código para conectar-se a um endpoint de API.

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.

2. Podemos fazer desenvolvimento ágil sem TDD?

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

54 Construindo um aplicativo usando TDD

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.

• Livro de receitas de engenharia de sistemas baseados em modelos ágeis, ISBN 9781838985837

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:

• Iniciando TDD: Arrange-Act-Assert

• Definir um bom teste

• Detectando erros comuns

• Afirmando exceções

• Testando apenas métodos públicos

• Aprendendo com nossos testes

• Iniciando Wordz – nosso primeiro teste

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

56 Escrevendo nosso primeiro teste

Iniciando TDD: Arrange-Act-Assert


Os testes unitários não são nada misteriosos. Eles são apenas código, código executável escrito na mesma
linguagem em que você escreve seu aplicativo. Cada teste de unidade constitui o primeiro uso do código que
você deseja escrever. Ele chama o código exatamente como será chamado na aplicação real. O teste executa
esse código, captura todas as saídas que nos interessam e verifica se elas são o que esperávamos que fossem.
Como o teste usa nosso código exatamente da mesma maneira que o aplicativo real, obtemos feedback
instantâneo sobre quão fácil ou difícil é usar nosso código. Isso pode parecer óbvio, e é, mas é uma ferramenta
poderosa para nos ajudar a escrever código limpo e correto. Vamos dar uma olhada em um exemplo de teste
unitário e aprender como definir sua estrutura.

Definindo a estrutura de teste


É sempre útil ter modelos para seguir quando fazemos coisas e os testes unitários não são exceção.
Com base no trabalho comercial realizado no Chrysler Comprehensive Compensation Project, o inventor do
TDD, Kent Beck, descobriu que os testes unitários tinham certas características em comum. Isso foi resumido
como uma estrutura recomendada para código de teste, chamada Arrange-Act-Assert ou AAA.

A definição original de AAA


A descrição original do AAA pode ser encontrada aqui, no wiki C2: http://wiki.
c2.com/?ArrangeActAssert.

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.*;

classe pública NomeUsuárioTeste {

@Teste

public void converteToLowerCase() {


var nome de usuário = novo nome de usuário("SirJakington35179");

String real = nomedeusuário.asLowerCase();

assertThat(real).isEqualTo("sirjakington35179");
}
}
Machine Translated by Google

Iniciando TDD: Arrange-Act-Assert 57

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.

O teste de unidade em si é o método convertsToLowerCase() . Novamente, o nome descreve o que esperamos


que aconteça. Quando o código for executado com sucesso, o nome de usuário será convertido em letras minúsculas.
Os nomes são intencionalmente simples, claros e descritivos. Este método possui a anotação @Test da estrutura de teste JUnit5 . A anotação
informa ao JUnit que este é um teste que pode ser executado para nós.

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.

Concluir nosso teste é a etapa final de afirmação. O assertThat(real).


isEqualTo("sirjakington35179"); line é nossa etapa de afirmação aqui. Ele usa o assertThat()
e o método isEqualTo() da biblioteca de asserções fluentes AssertJ . Sua função é verificar se o
resultado que retornamos da etapa Agir corresponde ou não às nossas expectativas. Aqui,
estamos testando se todas as letras maiúsculas do nome original foram convertidas para minúsculas.

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

58 Escrevendo nosso primeiro teste

Figura 5.1 – Saída do executor de testes JUnit

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:

Figura 5.2 – Um teste JUnit aprovado

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.

Trabalhando de trás para frente a partir dos resultados

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

Definindo um bom teste 59

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.

Aumentando a eficiência do fluxo de trabalho

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.

Definindo um bom teste

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

60 Escrevendo nosso primeiro teste

Aplicando os PRIMEIROS princípios


Estes são um conjunto de cinco princípios que tornam os testes mais eficazes:

• 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

Definindo um bom teste 61

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.

Usando uma afirmação por teste

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.

Decidindo sobre o escopo de um teste de unidade

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

que o teste possa ser executado isoladamente.

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

62 Escrevendo nosso primeiro teste

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.

Detectando erros comuns


A visão tradicional de teste é como um processo para verificar se o código funciona como deveria.
Os testes de unidade são excelentes nisso e automatizam o processo de execução do código com entradas conhecidas
e verificação de saídas esperadas. Como somos humanos, todos cometemos erros de vez em quando ao escrever
código e alguns deles podem ter impactos significativos. Existem vários erros simples e comuns que podemos cometer
e os testes unitários são excelentes em detectar todos eles. Os erros mais prováveis são os seguintes:

• Erros pontuais

• Lógica condicional invertida

• Condições ausentes

• Dados não inicializados

• O algoritmo errado

• Verificações de igualdade quebradas

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:

classe pública Nome de usuário {


nome da string final privada;

nome de usuário público(String nome de usuário) {


nome = nome de usuário;

string pública asLowerCase() {


var resultado = new StringBuilder();

for (int i=1; i < nome.comprimento(); i++) {


char atual = nome.charAt(i);

if (atual > 'A' && atual < 'Z') {


resultado.append(atual + 'a' - 'A');
} outro {
resultado.append(atual);
Machine Translated by Google

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:

Figura 5.3 – Um erro comum de codificação

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

public void rejeitaShortName() {


assertThatExceptionOfType(InvalidNameException.class)
.isThrownBy(()->novo nome de usuário("Abc"));
}
Machine Translated by Google

64 Escrevendo nosso primeiro teste

Podemos considerar adicionar outro teste especificamente destinado a provar que um nome de quatro caracteres é aceito e
nenhuma exceção é lançada:

@Teste

public void aceitaNomeComprimentoMínimo() {

assertThatNoException()

.isThrownBy(()->novo nome de usuário("Abcd"));

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.

Testando apenas métodos públicos

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

Aprendendo com nossos testes 65

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

public boolean equals(Objeto outro) {


return EqualsBuilder.reflectionEquals(este, outro);
}

@Sobrepor

public int hashCode() {


retornar HashCodeBuilder.reflectionHashCode (este);
}

Você pode descobrir mais sobre esses métodos de biblioteca em https://commons.apache.org/


apropriado/commons-lang/.

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.

Aprendendo com nossos testes

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.

Uma etapa de organização confusa

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

66 Escrevendo nosso primeiro teste

Uma etapa confusa do ato

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.

Uma etapa de afirmação confusa

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.

Limitações dos testes unitários

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

Cobertura de código – uma métrica muitas vezes sem sentido

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.

Escrevendo os testes errados

É 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

68 Escrevendo nosso primeiro teste

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:

1. Escreva o seguinte código para iniciar nosso teste:

classe pública WordTest {

@Teste

public void oneIncorrectLetter() {

}
}

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

public void oneIncorrectLetter() {


nova Palavra("A");

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

Figura 5.4 – Criando um diálogo de classe

4. Clique em OK para criar o arquivo na árvore de origem dentro do pacote correto.

5. Agora, renomeamos o parâmetro do construtor Word :

classe pública Palavra { palavra


pública (String palavra correta) {
// Sem ação }

6. A seguir voltamos ao teste. Capturamos o novo objeto como uma variável local para que possamos testá-lo:

@Teste

public void oneIncorrectLetter() {


var palavra = new Palavra("A");

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

public void oneIncorrectLetter() {


var palavra = new Palavra("A");

palavra.adivinhar("Z");
}
Machine Translated by Google

70 Escrevendo nosso primeiro teste

8. Use o preenchimento automático para adicionar o método palpite() à classe Word :

Figura 5.5 – Criando a classe Word

9. Clique em Enter para adicionar o método e altere o nome do parâmetro para um nome descritivo:

public void palpite(tentativa de string) {

10. A seguir, vamos adicionar uma maneira de obter a pontuação resultante dessa estimativa. Comece com o teste:

@Teste

public void oneIncorrectLetter() {


var palavra = new Palavra("A");

var pontuação = word.guess("Z");

Então, precisamos pensar um pouco sobre o que retornar do código de produção.

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 cinco getters, cada um retornando um enum. •

Um tipo de registro Java 17 com os mesmos getters.

• 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:

• Suportar um número variável de letras em uma palavra

• Representando a pontuação usando uma enumeração simples de INCORRETO, PART_CORRECT ou CORRETO

• 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:

1. Capture estas decisões no teste:

@Teste

public void oneIncorrectLetter() {


var palavra = new Palavra("A");

var pontuação = word.guess("Z");

var resultado = pontuação.letter(0);

assertThat(resultado).isEqualTo(Letter.INCORRECT);
}
Machine Translated by Google

72 Escrevendo nosso primeiro teste

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.

3. Vamos fazer esse teste passar, adicionando código à classe Word:

classe pública Palavra {


Palavra pública(String Palavra correta) {
// Não implementado
}

estimativa de pontuação pública (tentativa de string) {


var pontuação = new Pontuação();
pontuação de retorno;

}
}

4. Em seguida, crie a pontuação da classe:

Pontuação da classe pública {


carta pública carta (posição int) {
retornar Carta.INCORRETO;

}
}

Novamente, usamos atalhos IDE para fazer a maior parte do trabalho de escrever esse código para nós. O teste passa:

Figura 5.6 – Um teste aprovado no IntelliJ


Machine Translated by Google

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

74 Escrevendo nosso primeiro teste

2. Devemos nos limitar a uma classe de teste por classe de produção?

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.

3. Sempre usamos a estrutura AAA?

É 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 .

4. Os testes são códigos descartáveis?

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.

Neste capítulo, abordaremos os seguintes tópicos:

• Seguindo o ciclo RGR

• Escrevendo nossos próximos testes para Wordz

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.

Seguindo o ciclo RGR


Vimos no capítulo anterior como um único teste de unidade é dividido em três partes, conhecidas como
seções Arrange, Act e Assert. Isso forma um ritmo simples de trabalho que nos guia na escrita de cada
Machine Translated by Google

76 Seguindo os ritmos do TDD

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

Figura 6.1 – A fase vermelha

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

Seguindo o ciclo RGR 77

Mantenha a simplicidade – mudando para o verde

Figura 6.2 – A fase verde

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:

Figura 6.3 – O interior e o exterior de um componente caixa preta


Machine Translated by Google

78 Seguindo os ritmos do TDD

Como nossa implementação é encapsulada, podemos mudar de ideia mais tarde, à medida que aprendemos
mais, sem interromper o teste.

Existem duas diretrizes para esta fase:

• 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.

Refatorando para limpar código

Figura 6.4 – A fase de refatoração

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:

• Extraindo um método para remover código duplicado

• Renomear um método para expressar melhor o que ele faz

• Renomear uma variável para expressar melhor o que ela contém

• Dividir um método longo em vários métodos menores

• Extraindo uma classe menor

• Combinar uma longa lista de parâmetros em sua própria classe


Machine Translated by Google

Escrevendo nossos próximos testes para Wordz 79

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.

Escrevendo nossos próximos testes para 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

public void oneCorrectLetter() {


var palavra = new Palavra("A");

var pontuação = word.guess("A");

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:

classe pública Palavra {


palavra String final privada;

Palavra pública(String Palavra correta) {


Machine Translated by Google

80 Seguindo os ritmos do TDD

esta.palavra = palavra correta;


}

estimativa de pontuação pública (tentativa de string) {


var pontuação = nova pontuação (palavra);

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.

Usamos o IDE para nos ajudar a escrever a pontuação da classe:

Pontuação da classe pública {


string final privada correta;
resultado de carta privada = Letter.INCORRECT ;

pontuação pública (string correta) {


this.correct = correto;
}

carta pública carta (posição int) {


resultado de retorno;
}

public void avaliar(int posição, String tentativa) {


if (correto.charAt(posição) == tentativa.
charAt(posição)){
resultado = Letra.CORRETO;
}
Machine Translated by Google

Escrevendo nossos próximos testes para Wordz 81

}
}

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.

Execute todos os testes para ver como estamos:

Figura 6.5 – Dois testes aprovados

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.

Em termos de design, seguimos em frente. Passamos da letra codificada.INCORRETO


resultado com código que pode detectar suposições corretas e incorretas. Adicionamos o importante conceito
de design de um métodoassess() na classe Score. Isto é significativo. Nosso código agora revela um design;
o objeto Score saberá a palavra correta e será capaz de usar o método Assessment() contra a tentativa de
adivinhação. A terminologia usada aqui constitui uma boa descrição do problema que estamos resolvendo.
Queremos avaliar uma estimativa para retornar uma pontuação de palavra.

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

82 Seguindo os ritmos do TDD

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.

Definindo cheiros de código

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.

3. Vamos refatorar. Extraia um método isCorrectLetter() para maior clareza:

public void avaliar(int posição, String tentativa) {


if (isCorrectLetter(posição, tentativa)){
resultado = Letra.CORRETO;
}
}

private boolean isCorrectLetter(posição int,


Tentativa de sequência) {
retorne correto.charAt(posição) ==
tentativa.charAt(posição);
}

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

Escrevendo nossos próximos testes para Wordz 83

A legibilidade acontece durante a escrita e não a leitura


Uma pergunta comum de iniciantes em codificação é “Como posso melhorar minha capacidade de ler código?”

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.

Código mal escrito é difícil de ler. Infelizmente, é fácil escrever.

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

public void oneCorrectLetter() {


var palavra = new Palavra("A");

var pontuação = word.guess("A");

assertScoreForLetter(pontuação, 0, Letter.CORRECT);
}

private void assertScoreForLetter (pontuação de pontuação,


posição int, letra esperada) {
assertThat(pontuação.letra(posição))
.isEqualTo(esperado);
}

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

84 Seguindo os ritmos do TDD

4. Vamos mudar a forma como especificamos a posição da letra a ser verificada no método Assessment() :

Pontuação da classe pública {


string final privada correta;
resultado de carta privada = Letter.INCORRECT ;
posição interna privada;

pontuação pública (string correta) {


this.correct = correto;
}

carta pública carta (posição int) {


resultado de retorno;
}

avaliação de void pública (tentativa de string) {


if (isCorrectLetter(tentativa)){
resultado = Letra.CORRETO;
}
}

private boolean isCorrectLetter(tentativa de string) {


retorne correto.charAt(posição) == tentativa.
charAt(posição);
}
}

Removemos o parâmetro position do método assessment() e o convertemos em um campo


chamado position. A intenção é simplificar o uso do método avaliar() . Já não é necessário indicar
explicitamente qual a posição que está a ser avaliada. Isso torna o código mais fácil de chamar. O
código que acabamos de adicionar só funcionará no caso em que a posição for zero. Tudo bem,
pois é a única coisa exigida pelos nossos testes nesta fase. Faremos esse código funcionar para
valores diferentes de zero posteriormente.

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

Escrevendo nossos próximos testes para Wordz 85

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.

Avançando no design com combinações de duas letras

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:

avaliação de void pública (tentativa de string) {


for (char atual: tentativa.toCharArray()) {
if (isCorrectLetter(atual)) {
resultado = Letra.CORRETO;
}
}
}

private boolean isCorrectLetter(char currentLetter) {


return correto.charAt(posição) == cartaatual;
}

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

86 Seguindo os ritmos do TDD

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:

avaliação de void pública (tentativa de string) {


for (char atual: tentativa.toCharArray()) {
if (isCorrectLetter(atual)) {
resultado = Letra.CORRETO;
} else if (occursInWord(atual)) {
resultado = Letra.PART_CORRECT;
}
}
}
private boolean ocorre no Word(char atual) {
retornar
correto.contains(String.valueOf(atual));
}

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

Escrevendo nossos próximos testes para Wordz 87

4. Adicione um novo teste exercitando todas as três possibilidades de pontuação:

@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:

Pontuação da classe pública {


string final privada correta;
lista final privada <Carta> resultados =
new ArrayList<>();
posição interna privada;

pontuação pública (string correta) {


this.correct = correto;
}

carta pública carta (posição int) { return results.get


(posição);
}

public void avaliar(String tentativa) { for (char atual:


tentativa.toCharArray()) {
if (isCorrectLetter(atual)) {
resultados.add(Carta.CORRETO); } else if
(occursInWord(atual)) {
resultados.add(Carta.PART_CORRECT); } outro {
Machine Translated by Google

88 Seguindo os ritmos do TDD

resultados.add(Letra.INCORRETO);

posição++;
}
}

private boolean ocorre no Word(char atual) {


retornar

correto.contains(String.valueOf(atual));
}

booleano privado isCorrectLetter(char


carta atual) {
retorne correto.charAt(posição) ==
carta 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.

Na classe de código de produção Score, é o corpo do loop do método Assessment() que


parece complicado. Ele tem um corpo de loop longo com lógica e um conjunto de blocos if-
else-if . Parece que o código poderia ficar mais claro. Podemos extrair o corpo do loop em um método.
O nome do método nos dá um local para descrever o que está acontecendo com cada coisa. O loop então
se torna mais curto e mais simples de entender. Também podemos substituir as escadas if-else-if por uma
construção mais simples.
Machine Translated by Google

Escrevendo nossos próximos testes para Wordz 89

6. Vamos extrair a lógica dentro do corpo do loop para um método scoreFor() :

avaliação de void pública (tentativa de string) {


for (char atual: tentativa.toCharArray()) {
resultados.add(pontuaçãoFor(atual));

posição++;
}
}

carta privada scoreFor(char atual) {


if (isCorrectLetter(atual)) {
retornar Carta.CORRETO;

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.

7. Vamos remover o código de asserção duplicado extraindo um método:

@Teste

void allScoreCombinations() {
var palavra = new Palavra("ARI");
var pontuação = word.guess("ZAI");
assertScoreForGuess(pontuação, INCORRETO,
Machine Translated by Google

90 Seguindo os ritmos do TDD

PART_CORRETO,
CORRETO);

private void assertScoreForGuess (Pontuação , Carta…


for (int posição = 0;
posição < pontuações esperadas.comprimento; posição+
+){ Letra esperada
= pontuações esperadas[posição];

assertThat(pontuação.letra(posição))
.isEqualTo(esperado);
}
}

Ao extrair o método assertScoreForGuess() , criamos uma maneira de verificar as pontuações para um


número variável de letras. Isso elimina aquelas linhas de afirmação copiadas e coladas que tínhamos e
aumenta o nível de abstração. O código de teste é lido com mais clareza à medida que agora descrevemos
os testes em termos da ordem INCORRETO, PART_CORRECT, CORRETO em que esperamos que a
pontuação esteja. Ao adicionar uma importação estática a essas enums, a confusão de sintaxe também é beneficamente reduzida.

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;

classe pública WordTest {


@Teste

public void oneIncorrectLetter() {


var palavra = new Palavra("A");
var pontuação = word.guess("Z");
assertScoreForGuess(pontuação, INCORRETO);

@Teste
Machine Translated by Google

Escrevendo nossos próximos testes para Wordz 91

public void oneCorrectLetter() {


var palavra = new Palavra("A");
var pontuação = word.guess("A");
assertScoreForGuess(pontuação, CORRETO);
}

@Teste

public void secondLetterWrongPosition() { var palavra = new


Word("AR");
var pontuação = word.guess("ZA");
assertScoreForGuess(pontuação, INCORRETO,
PARTE_CORRETO);
}

@Teste

public void allScoreCombinations() {


var palavra = new Palavra("ARI");
var pontuação = word.guess("ZAI");
assertScoreForGuess(pontuação, INCORRETO,
PART_CORRETO,
CORRETO);
}

private void assertScoreForGuess(pontuação de pontuação, letra...


pontuações esperadas) { for (int position = 0;

posição < pontuações esperadas.comprimento;


posição++) {
Letra esperada = pontuação
esperada[posição];
assertThat(pontuação.letra(posição)) .isEqualTo(esperado);

}
}
}
Machine Translated by Google

92 Seguindo os ritmos do TDD

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.

2. Como podemos escrever testes antes do código?

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.

3. Os testes devem ser códigos descartáveis?

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.

4. Precisamos refatorar após cada aprovação no teste?

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

• Ficando Verde no Vermelho

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

• Refatoração: Melhorando o design do código existente, Martin Fowler (ISBN 978-0134757599)

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.

• Documentação AssertJ para matchers personalizados

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

96 Projeto de condução – TDD e SOLID

Neste capítulo, vamos cobrir os seguintes tópicos principais:

• Guia de teste – nós orientamos o design

• Princípio de Responsabilidade Única (SRP) – blocos de construção

simples • Princípio de Inversão de Dependência (DIP) – ocultando detalhes irrelevantes

• Princípio de Substituição de Liskov (LSP) – objetos trocáveis

• Princípio Aberto-Fechado (OCP) – design extensível

• Princípio de segregação de interface (ISP) – interfaces eficazes

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.

Guia de teste – nós orientamos o design


No Capítulo 5, Escrevendo nosso primeiro teste, escrevemos nosso primeiro teste. Para fazer isso, tomamos uma série
de decisões de design. Vamos revisar o código de teste inicial e listar todas as decisões de design que tivemos que
tomar, como segue:

@Teste

public void oneIncorrectLetter() {


var palavra = new Palavra("A");

var pontuação = word.guess("Z");

assertThat(pontuação.letter(0)).isEqualTo(Letter.INCORRECT);
}

Decidimos o seguinte:

• O que testar

• Como chamar o teste

• Como chamar o método em teste

• Em qual classe colocar esse método

• A assinatura desse método


Machine Translated by Google

SRP – blocos de construção simples 97

• A assinatura do construtor da classe

• Quais outros objetos devem colaborar

• As assinaturas dos métodos envolvidos nessa colaboração

• Qual será a forma da saída deste método

• Como acessar esse resultado e afirmar que funcionou

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.

SRP – blocos de construção simples


Nesta seção, examinaremos o primeiro princípio, conhecido como SRP. Usaremos um único exemplo
de código em todas as seções. Isso esclarecerá como cada princípio é aplicado a um projeto
orientado a objetos (OO) . Veremos um exemplo clássico de design OO: desenhar formas. O
diagrama a seguir é uma visão geral do design em Unified Modeling Language (UML), descrevendo
o código apresentado no capítulo:
Machine Translated by Google

98 Projeto de condução – TDD e SOLID

Figura 7.1 – Diagrama UML para código de formas

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

SRP – blocos de construção simples 99

Muitas responsabilidades tornam o código mais difícil de trabalhar


Um erro comum de programação é combinar muitas responsabilidades em um único código.
Se tivermos uma classe que pode gerar HTML ( Hypertext Markup Language ), executar uma regra de negócios e
buscar dados de uma tabela de banco de dados, essa classe terá três motivos para mudar. Sempre que for necessária
uma alteração em uma dessas áreas, correremos o risco de fazer uma alteração no código que quebre os outros dois aspectos.
O termo técnico para isso é que o código é altamente acoplado. Isso faz com que mudanças em uma área se
repercutam e afetem outras áreas.

Podemos visualizar isso como bloco de código A no diagrama a seguir:

Figura 7.2 – Componente único: múltiplas razões para mudar

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.

Podemos visualizar isso no seguinte diagrama:

Figura 7.3 – Múltiplos componentes: um motivo para mudar


Machine Translated by Google

100 Projeto de condução – TDD e SOLID

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 aplicação do SRP oferece outros benefícios, como segue:

• Capacidade de reutilizar código

• Manutenção futura simplificada

Capacidade de reutilizar código

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.

Manutenção futura simplificada

À 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.

Código duplicado é um problema de manutenção – complica futuras alterações de código. Se copiarmos e


colarmos uma seção de código três vezes, digamos, parece bastante óbvio para nós no momento o que estamos fazendo.
Temos um conceito que precisa acontecer três vezes, então colamos três vezes. Mas quando chega a hora de ler o código novamente,
esse processo de pensamento foi perdido. Ele apenas é lido como três pedaços de código não relacionados. Perdemos informações de
engenharia ao copiar e colar. Precisaremos fazer engenharia reversa desse código para descobrir que há três lugares onde precisamos
alterá-lo.
Machine Translated by Google

SRP – blocos de construção simples 101

Contra-exemplo – molda o código que viola o SRP


Para ver o valor da aplicação do SRP, vamos considerar um trecho de código que não o utiliza. O trecho de código
a seguir tem uma lista de formas que são desenhadas quando chamamos o método draw() :

classe pública Formas {


private final List<Forma> allShapes = new ArrayList<>();

public void add(Formas) {


todas as formas.add(s);
}

public void draw (Gráficos g) { for (Shape s:


allShapes) { switch (s.getType()) {

caso "caixa de texto":


vart = (TextBox)s;
g.drawText(t.getText());
quebrar;

case "retângulo": var r =


(retângulo) s; for (int linha = 0;

linha <r.getHeight(); linha++) {

g.drawLine(0, r.getWidth());
}
}
}
}
}

Podemos perceber que este código tem quatro responsabilidades, sendo elas:

• Gerenciando a lista de formas com o método add()


• Desenhando todas as formas na lista com o método draw()
• Conhecer cada tipo de forma na instrução switch • Possui

detalhes de implementação para desenhar cada tipo de forma nas instruções case
Machine Translated by Google

102 Projeto de condução – TDD e SOLID

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.

Aplicando SRP para simplificar manutenções futuras


Refatoraremos esse código para aplicar o SRP, seguindo pequenos passos. A primeira coisa a fazer é transferir esse
conhecimento de como desenhar cada tipo de forma desta classe, da seguinte forma:

formatos de embalagens;

importar java.util.ArrayList; importar


java.util.List;

classe pública Formas {


private final List<Forma> allShapes = new ArrayList<>();

public void add(Formas) {


todas as formas.add(s);
}

public void draw (Gráficos g) { for (Shape s:


allShapes) { switch (s.getType()) {

caso "caixa de texto":


vart = (TextBox)s;
t.draw(g);
quebrar;

case "retângulo": var r =


(retângulo) s; r.draw(g);

}
}
}
}
Machine Translated by Google

SRP – blocos de construção simples 103

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:

classe pública Retângulo {


largura interna final privada;
altura interna final privada;

retângulo público(int largura, int altura){


esta.largura = largura;
esta.altura = altura;
}

public void draw(Gráficos g) {


for (int linha=0; linha < altura; linha++) {
g.drawHorizontalLine(largura);
}
}
}

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

104 Projeto de condução – TDD e SOLID

Organizar testes para ter uma única responsabilidade

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.

Separando testes com configurações diferentes


Às vezes, um grupo de objetos pode ser organizado para colaborar de diversas maneiras diferentes. Os
testes para este grupo geralmente são melhores se escrevermos um único teste por configuração. Acabamos
com vários testes menores que são mais fáceis de trabalhar.

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.

DIP – ocultando detalhes irrelevantes

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;

classe pública Formas {


Machine Translated by Google

DIP – ocultando detalhes irrelevantes 105

private final List<Forma> allShapes = new ArrayList<>();

public void add(Formas) {


todas as formas.add(s);
}

public void draw(Gráficos g) {


for (Formas: todas as Formas) {
mudar (s.getType()) {
caso "caixa de texto":
vart = (TextBox)s;
t.draw(g);
quebrar;

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

106 Projeto de condução – TDD e SOLID

Figura 7.4 – Dependendo dos detalhes

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

• A classe Shapes ficará mais longa e menos fácil de ler

• Acabaremos com mais casos de teste

• Cada caso de teste será acoplado a classes concretas como Rectangle

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.

Aplicando DI ao código de formas


Podemos melhorar o código das formas aplicando o Princípio de Inversão de Dependência (DIP) descrito no
capítulo anterior. Vamos adicionar um método draw() à nossa interface Shape , como segue:

formatos de embalagens;

interface pública Forma {


desenho vazio (Gráficos g);
}
Machine Translated by Google

DIP – ocultando detalhes irrelevantes 107

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:

classe pública Retângulo implementa Shape { private final int


width; altura interna final privada;

retângulo público(int largura, int altura){


esta.largura = largura;
esta.altura = altura;
}

@Sobrepor

public void draw(Gráficos g) { for (int linha=0;


linha < altura; linha++) {
g.drawHorizontalLine(largura);
}
}
}

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.

Podemos refatorar a classe Shapes para ficar assim:

classe pública Formas {


private final List<Forma> all = new ArrayList<>();

public void add(Formas) { all.add(s);

public void draw(Gráficos gráficos) {


all.forEach(forma->forma.draw(gráficos));
Machine Translated by Google

108 Projeto de condução – TDD e SOLID

}
}

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:

classe pública Formas {


private final List<Forma> all = new ArrayList<>();
gráficos finais privados; gráficos;

formas públicas (gráficos gráficos) {


this.graphics = gráficos;
}

public void add(Formas) {


todos.add(s);
}

sorteio de vazio público() {


all.forEach(forma->forma.draw(gráficos));
}
}

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

LSP – objetos trocáveis 109

Figura 7.5 – Invertendo dependências

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

Faça o código depender de abstrações e não de detalhes.

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.

LSP – objetos trocáveis


A vencedora do Prêmio Turing, Barbara Liskov, é a criadora de uma regra relativa à herança que agora é comumente
conhecida como LSP. Isso foi provocado por uma questão em POO: se pudermos estender uma classe e usá- la no lugar
da classe que estendemos, como podemos ter certeza de que a nova classe não quebrará as coisas?

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

110 Projeto de condução – TDD e SOLID

É 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 !):

classe pública MaliciousShape implementa Shape {


@Sobrepor

public void draw(Gráficos g) {


tentar {
String[] deletarTudo = {"rm", "-Rf", "*"};
Runtime.getRuntime().exec(deleteEverything,null);

g.drawText("Nada para ver aqui...");


} catch (Exceção ex) {
// Nenhuma ação
}
}
}

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.

Definição formal de LSP

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

LSP – objetos trocáveis 111

Revendo o uso de LSP no código de formas


Todas as classes que implementam Shape estão em conformidade com LSP. Isso fica claro na classe TextBox , como
podemos ver aqui:

classe pública TextBox implementa Shape {


Texto de string final privado;

public TextBox(String texto) {


este.texto = texto;
}

@Sobrepor

public void draw(Gráficos g) {


g.drawText(texto);
}
}

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

112 Projeto de condução – TDD e SOLID

OCP – design extensível

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.

Vamos começar com o código original da classe Shapes, como segue:

classe pública Formas {


private final List<Forma> allShapes = new ArrayList<>();

public void add(Formas) {


todas as formas.add(s);
}

public void draw (Gráficos g) { for (Shape s:


allShapes) { switch (s.getType()) {

caso "caixa de texto":


vart = (TextBox)s;
g.drawText(t.getText());
quebrar;

case "retângulo": var r =


(retângulo) s; for (int linha = 0;

linha <r.getHeight(); linha++) {

g.drawLine(0, r.getWidth());
}
}
}
}
}
Machine Translated by Google

OCP – design extensível 113

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.

Modificar o código existente tem diversas desvantagens, conforme descrito aqui:

• Invalidamos testes anteriores. Este agora é um código diferente do que testamos.

• Poderemos introduzir um erro que quebre parte do suporte existente para formas.

• O código ficará mais longo e mais difícil de ler.

• Podemos ter vários desenvolvedores adicionando formas ao mesmo tempo e obter um conflito de mesclagem quando
nós combinamos o trabalho deles.

Aplicando DIP e refatorando o código, terminamos com isto:

classe pública Formas {


private final List<Forma> all = new ArrayList<>();
gráficos finais privados; gráficos;

formas públicas (gráficos gráficos) {


this.graphics = gráficos;
}

public void add(Formas) {


todos.add(s);
}

sorteio de vazio público() {


all.forEach(forma->forma.draw(gráficos));
}
}

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

114 Projeto de condução – TDD e SOLID

Adicionando um novo tipo de forma

Para ver como isso funciona na prática, vamos criar um novo tipo de forma, a classe RightArrow , como segue:

classe pública RightArrow implementa Shape {


public void draw(Gráficos g) {
g.drawText( " \");
g.drawText("-----");
g.drawText( " /" );
}
}

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;

classe pública ShapesExample {


public static void main(String[] args) {
new ShapesExample().run();
}

execução void privada() {


Console gráfico = new ConsoleGraphics();
var formas = new Formas(console);

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

ISP – interfaces eficazes 115

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.

ISP – interfaces eficazes


Nesta seção, veremos um princípio que nos ajuda a escrever interfaces eficazes. É conhecido como 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.

Revendo o uso do ISP no código de formas


O uso mais notável do ISP está na interface Shape , conforme ilustrado aqui:

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

116 Projeto de condução – TDD e SOLID

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:

interface pública Gráficos {


void drawText(String texto);
void drawHorizontalLine(int largura);
}

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:

classe pública ConsoleGraphics implementa Gráficos {


@Sobrepor

public void drawText(String texto) {


imprimir(texto);
}

@Sobrepor

public void drawHorizontalLine(int largura) {


var rowText = new StringBuilder();

for (int i = 0; i < largura; i++) {


rowText.append('X');
}

imprimir(rowText.toString());
}

private void print(String texto) {


System.out.println(texto);
}
}
Machine Translated by Google

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

Mantenha as interfaces pequenas e fortemente relacionadas a uma única ideia.

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.

2. Precisamos usar princípios SOLID com TDD?

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

118 Projeto de condução – TDD e SOLID

3. Os princípios SOLID são os únicos que devemos usar?

Não. Devemos usar todas as técnicas à nossa disposição.

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

4. Se não utilizarmos os princípios SOLID, ainda poderemos fazer TDD?

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.

5. Como o SRP se relaciona com o ISP?

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.

6. Como o OCP se relaciona com o DIP e o LSP?

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.

Neste capítulo, vamos cobrir os seguintes tópicos principais:

• Os problemas de testar colaboradores

• O objetivo do teste duplica

• Usando stubs para resultados predefinidos

• Usando simulações para verificar interações

• Compreender quando os testes duplos são apropriados

• Trabalhando com Mockito – uma biblioteca de simulação popular

• Gerando código de tratamento de erros usando stubs

• Testando uma condição de erro no Wordz

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

120 Duplas de teste – Stubs e simulações

Os problemas que os colaboradores apresentam para testes


Nesta seção, entenderemos os desafios que surgem à medida que desenvolvemos nosso software em uma base de
código maior. Revisaremos o que significa um objeto de colaboração e, em seguida, daremos uma olhada em dois
exemplos de colaborações que são difíceis de testar.

À 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.

Os desafios de testar comportamentos irrepetíveis


Aprendemos que as etapas básicas de um teste TDD são Organizar, Agir e Afirmar. Pedimos ao objeto que aja e
então afirmamos que um resultado esperado acontece. Mas o que acontece quando esse resultado é imprevisí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;

classe pública DiceRoll {

final privado int NUMBER_OF_SIDES = 6;


final privado RandomGenerator rnd =
RandomGenerator.getDefault();

public String asText() {


int rolado = rnd.nextInt(NUMBER_OF_SIDES) + 1;

return String.format("Você rolou um %d", rolou);


}
}
Machine Translated by Google

Os problemas que os colaboradores apresentam para testes 121

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.

Os desafios de testar o tratamento de erros


Testar código que lida com condições de erro é outro desafio. A dificuldade aqui não reside em afirmar que
o erro foi tratado, mas sim no desafio de como fazer com que esse erro aconteça dentro do objeto
colaborador.

Para ilustrar, vamos imaginar um código para nos avisar quando a bateria do nosso dispositivo portátil está fraca:

classe pública BatteryMonitor {


public void warningWhenBatteryPowerLow() {
if (DeviceApi.getBatteryPercentage() <10) {
System.out.println("Aviso - Bateria 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.

Entendendo por que essas colaborações são desafiadoras


Ao fazer TDD, queremos testes rápidos e repetíveis. Qualquer cenário que envolva comportamento imprevisível
ou exija que controlemos uma situação sobre a qual não temos controle claramente causa problemas para 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

122 Duplas de teste – Stubs e simulações

O objetivo do teste duplica


Nesta seção, aprenderemos técnicas que nos permitem testar essas colaborações desafiadoras.
Apresentaremos a ideia de duplas de teste. Aprenderemos como aplicar os princípios SOLID para projetar
código flexível o suficiente para usar esses testes duplos.

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?

1. Crie uma interface que abstraia a fonte dos números aleatórios:

interface Números Aleatórios {


int nextInt(int superiorBoundExclusivo);
}

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;

classe pública DiceRoll {

final privado int NUMBER_OF_SIDES = 6;


números aleatórios finais privados rnd;

public DiceRoll (Números Aleatórios r) {


isto.rnd = r;
}
public String asText() {
int rolado = rnd.nextInt(NUMBER_OF_SIDES) + 1;

return String.format("Você rolou um %d",


Machine Translated by Google

O objetivo do teste duplica 123

enrolado);

}
}

Invertemos a dependência do gerador de números aleatórios, substituindo-o pela interface


RandomNumbers . Adicionamos um construtor que permite RandomNumbers adequados
implementação a ser injetada. Atribuímos isso ao campo rnd . O método asText() agora chama o método
nextInt() em qualquer objeto que passamos para o construtor.

3. Escreva um teste, usando um teste duplo para substituir a fonte RandomNumbers :

exemplos de pacotes;

importar org.junit.jupiter.api.Test; importar estático


org.assertj.core.api.Assertions.assertThat;

classe DiceRollTest {
@Teste

void produzMensagem() {
var stub = new StubRandomNumbers();
var roll = new DiceRoll(stub);

var real = roll.asText();

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;

A classe pública StubRandomNumbers implementa RandomNumbers {

@Sobrepor

public int nextInt(int UpperBoundExclusive) { return 4; // @veja https://


xkcd.com/221
}
}
Machine Translated by Google

124 Duplas de teste – Stubs e simulações

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.

Fazendo a versão de produção do código


Para fazer a classe DiceRoll funcionar corretamente na produção, precisaríamos injetar uma fonte genuína
de números aleatórios. Uma classe adequada seria a seguinte:

classe pública RandomlyGeneratedNumbers implementa RandomNumbers {

final privado RandomGenerator rnd =


RandomGenerator.getDefault();

@Sobrepor

public int nextInt(int UpperBoundExclusive) {

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

O objetivo do teste duplica 125

Inversão de dependência, injeção de dependência e inversão de controle


O exemplo anterior mostra essas três ideias em ação. A inversão de dependência é a técnica de
design onde criamos uma abstração em nosso código. A injeção de dependência é a técnica de
tempo de execução em que fornecemos uma implementação dessa abstração para o código que depende dela.
Juntas, essas ideias são frequentemente denominadas Inversão de Controle (IoC). Estruturas como Spring
às vezes são chamados de contêineres IoC porque fornecem ferramentas para ajudá-lo a gerenciar a
criação e injeção de dependências em um aplicativo.

O código a seguir é um exemplo de como usaríamos DiceRoll e RandomlyGeneratedNumbers


em produção:

classe pública DiceRollApp {


public static void main(String[] args) {
novo DiceRollApp().run();
}

execução void privada() {


var rnd = new RandomlyGeneratedNumbers();
var roll = new DiceRoll(rnd);

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

126 Duplas de teste – Stubs e simulações

Usando stubs para resultados pré-preparados


A seção anterior explicou que os test doubles eram um tipo de objeto que poderia substituir um objeto de
produção para que pudéssemos escrever um teste com mais facilidade. Nesta seção, examinaremos mais de
perto esse teste duplo e generalizá-lo-emos.

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:

Figura 8.1 – Substituindo um colaborador por um stub

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

Usando simulações para verificar interações 127

Quando usar objetos stub


Stubs são úteis sempre que nosso SUT usa um modelo pull de colaboração com uma dependência. Alguns
exemplos de quando usar stubs faz sentido são os seguintes:

• 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

ou JSON, forneça dados de entrada com um stub

• 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

aleatórios por uma chamada para um stub

• Stubbing de sistemas de autenticação para sempre permitir que um usuário de teste faça login: Substitua chamadas para

sistemas de autenticação por stubs simples de “login bem-sucedido”

• Stubbing de respostas de um serviço web de terceiros, como um provedor de pagamento: substitua


chamadas reais para serviços de terceiros com chamadas para um stub

• Stubbing de uma chamada para um comando do sistema operacional: Substitua uma chamada para o sistema operacional para,

por exemplo, listar um diretório com dados de stub predefinidos

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.

Usando simulações para verificar interações


Nesta seção, daremos uma olhada em outro tipo importante de teste duplo: o objeto simulado. Os objetos
simulados resolvem um problema ligeiramente diferente dos objetos stub, como veremos nesta 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

foi emitido, juntamente com quaisquer parâmetros necessários.


Machine Translated by Google

128 Duplas de teste – Stubs e simulações

O diagrama de objetos UML a seguir mostra o arranjo geral:

Figura 8.2 – Substituir colaborador por mock

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:

interface pública MailServer {


void sendEmail(String destinatário, String assunto,
Texto de sequência);
}

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 :

A classe pública MockMailServer implementa MailServer {


booleano foi chamado;
String realRecipient;
String atualSubject;
String textoactual;

@Sobrepor

public void sendEmail(String destinatário, String assunto,


Texto de sequência) {
foiChamado = verdadeiro;
realRecipient = destinatário;
atualAssunto = assunto;
TextoAtual = texto;
Machine Translated by Google

Usando simulações para verificar interações 129

}
}

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

public void enviaBem-vindoEmail() {


var mailServer = new MockMailServer();
var notificações = new UserNotifications(mailServer);

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

130 Duplas de teste – Stubs e simulações

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.

Compreender quando os testes duplos são apropriados


Objetos simulados são um tipo útil de teste duplo, como vimos. Mas nem sempre são a abordagem correta. Existem
algumas situações em que devemos evitar ativamente o uso de simulações. Essas situações incluem o uso excessivo
de simulações, o uso de simulações para código que você não possui e a simulação de objetos de valor. Veremos
essas situações a seguir. Em seguida, recapitularemos com conselhos gerais sobre onde as simulações normalmente são úteis.
Vamos começar considerando os problemas causados pelo uso excessivo de objetos simulados.

Evitando o uso excessivo de objetos simulados

À 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.

Não zombe de código que você não possui

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

Compreender quando os testes duplos são apropriados 131

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.

Não zombe de objetos de valor


Um objeto de valor é um objeto que não possui identidade específica, é definido apenas pelos dados que
contém. Alguns exemplos incluiriam um número inteiro ou um objeto string. Consideramos duas strings
iguais se contiverem o mesmo texto. Eles podem ser dois objetos string separados na memória, mas se
tiverem o mesmo valor, os consideraremos iguais.

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.

Você não pode zombar sem injeção de dependência

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;

classe pública UserGreeting {

perfis finais privados de UserProfiles


= new UserProfilesPostgres();

public String formatGreeting(ID do usuário) {


return String.format("Olá e seja bem-vindo, %s",
perfis.fetchNicknameFor(id));
}
}
Machine Translated by Google

132 Duplas de teste – Stubs e simulações

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.

Não teste a simulação

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.

Quando usar objetos simulados

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

• Inserir ou excluir dados de um banco de dados

• Enviando um comando através de um soquete TCP ou interface serial

• Invalidando um cache

• Gravar informações de log em um arquivo de log ou distribuir endpoint de log

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

Trabalhando com Mockito – uma biblioteca de simulação popular 133

Trabalhando com Mockito – uma biblioteca de simulação popular


As seções anteriores mostraram exemplos de uso de stubs e mocks para testar código. Temos escrito
esses testes duplos à mão. Obviamente, é bastante repetitivo e demorado fazer isso. Isso levanta a
questão de saber se esse código repetitivo pode ser eliminado por automatização. Felizmente para
nós, isso pode. Esta seção revisará a ajuda disponível na popular biblioteca Mockito.

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/.

Primeiros passos com o Mockito

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.

O trecho de build.gradle é assim:

dependências {
testImplementation 'org.junit.jupiter:junit-jupiter api:5.8.2'

testRuntimeOnly 'org.junit.jupiter:junit-jupiter engine: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'

Escrevendo um esboço com Mockito

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:

1. Escreva a classe de teste JUnit5 básica e integre-a ao Mockito:

exemplos de pacotes

importar org.junit.jupiter.api.extension.ExtendWith;
importar org.mockito.junit.jupiter.MockitoExtension;
Machine Translated by Google

134 Duplas de teste – Stubs e simulações

@ExtendWith(MockitoExtension.class) classe pública

UserGreetingTest { }

@ExtendWith(MockitoExtension.class) marca este teste como usando Mockito. Quando executamos


este teste JUnit5, a anotação garante que o código da biblioteca Mockito seja executado.

2. Adicione um teste confirmando o comportamento esperado. Capturaremos isso em uma afirmação:

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;

@ExtendWith(MockitoExtension.class) classe pública

UserGreetingTest {

@Teste

void formatosGreetingWithName() {

String real = «»;


afirmarIsso(real)

.isEqualTo("Olá e seja bem-vindo, Alan");


}
}

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;

importar org.junit.jupiter.api.Test; importar


org.junit.jupiter.api.extension.ExtendWith; importar org.mockito.junit.jupiter.MockitoExtension;
Machine Translated by Google

Trabalhando com Mockito – uma biblioteca de simulação popular 135

importar estático org.assertj.core.api.Assertions.assertThat;

@ExtendWith(MockitoExtension.class)

classe pública UserGreetingTest {

UserId final estático privado USER_ID


= novo UserId("1234");

@Teste

void formatosGreetingWithName() {

var saudação = new UserGreeting();

String real =
saudação.formatGreeting(USER_ID);

afirmarIsso(real)

.isEqualTo("Olá e seja bem-vindo, Alan");

}
}

Esta etapa elimina as duas novas classes de código de produção, conforme mostrado nas etapas a seguir.

4. Adicione um esqueleto de classe UserGreeting :

exemplos de pacotes;

classe pública UserGreeting { public String


formatGreeting (ID do usuário) {
lançar new UnsupportedOperationException();
}
}

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

136 Duplas de teste – Stubs e simulações

5. Adicione um esqueleto de classe UserId:

exemplos de pacotes;

classe pública UserId {

ID do usuário público (ID da string) { }

Novamente, obtemos um shell vazio apenas para compilar o teste. Então, executamos o teste e ele ainda falha:

Figura 8.3 – Falha no teste

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;

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;

@ExtendWith(MockitoExtension.class) classe pública


UserGreetingTest {

UserId final estático privado USER_ID


= novo UserId("1234");
Machine Translated by Google

Trabalhando com Mockito – uma biblioteca de simulação popular 137

perfis privados de UserProfiles;

@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");

}
}

Continuamos adicionando o código mínimo para compilar o teste. Se executarmos o teste,


ele ainda falhará. Mas progredimos ainda mais, então a falha agora é um erro
UnsupportedOperationException. Isso confirma que formatGreeting() foi chamado:

Figura 8.4 – Falha confirma chamada de método

7. Adicione comportamento ao método formatGreeting():

exemplos de pacotes;

classe pública UserGreeting {

perfis UserProfiles finais privados;

public UserGreeting(perfis de UserProfiles) {


this.profiles = perfis;
}
Machine Translated by Google

138 Duplas de teste – Stubs e simulações

public String formatGreeting(ID do usuário) {


return String.format("Olá e bem-vindo, %s",
perfis.fetchNicknameFor(id));
}
}

8. Adicione fetchNicknameFor() à interface UserProfiles :

exemplos de pacotes;

interface pública UserProfiles {


String fetchNicknameFor(ID do usuário);
}

9. Execute o teste. Irá falhar com uma exceção nula:

Figura 8.5 – Falha de exceção nula

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).

10. Adicione a anotação @Mock ao campo de perfis :

exemplos de pacotes;

importar org.junit.jupiter.api.Test; importar


org.junit.jupiter.api.extension.ExtendWith; importar org.mockito.Mock; importar
org.mockito.junit.jupiter.MockitoExtension;

importar estático org.assertj.core.api.Assertions.assertThat;

@ExtendWith(MockitoExtension.class) classe pública


UserGreetingTest {
Machine Translated by Google

Trabalhando com Mockito – uma biblioteca de simulação popular 139

UserId final estático privado USER_ID = novo


UserId("1234");

@Zombar

perfis privados de UserProfiles;

@Teste

void formatosGreetingWithName() {

var saudação = new UserGreeting(perfis);

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:

Figura 8.6 – Adicionado mock, não configurado

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

140 Duplas de teste – Stubs e simulações

importar org.mockito.Mockito; importar


org.mockito.junit.jupiter.MockitoExtension;

importar estático org.assertj.core.api.Assertions.assertThat;


importar estático org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class) classe pública

UserGreetingTest {

UserId final estático privado USER_ID = novo


UserId("1234");

@Zombar

perfis privados de UserProfiles;

@Teste

void formatosGreetingWithName()
{ quando(profiles.fetchNicknameFor(USER_ID))
.thenReturn("Alan");

var saudação = new UserGreeting(perfis);

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

Trabalhando com Mockito – uma biblioteca de simulação popular 141

Figura 8.7 – Aprovação no teste

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.

Escrevendo uma simulação com Mockito

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

servidor de correio privado mailServer;

@Teste

public void enviaBem-vindoEmail() {


var notificações

= novo UserNotifications(mailServer);

notificações.welcomeNewUser("test@example.com");

verificar(mailServer).sendEmail("teste@example.com",
"Bem-vindo!",
Machine Translated by Google

142 Duplas de teste – Stubs e simulações

"Bem-vindo à sua conta");

}
}

Neste teste, vemos a anotação @ExtendWith(MockitoExtension.class) para inicializar o Mockito


e o formato familiar Arrange, Act e Assert do nosso método de teste. A nova ideia aqui está na
afirmação. Usamos o método verify() da biblioteca Mockito para verificar se o método sendEmail()
foi chamado corretamente pelo nosso SUT. A verificação também verifica se foi chamado com
os valores de parâmetro corretos.

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.

Cuidado com a sintaxe when() e verify() do Mockito!


Mockito tem uma sintaxe sutilmente diferente para quando() e verificar():

* quando(object.method()).thenReturn(valor esperado);

* verificar(objeto).método();

Desfocando a distinção entre stubs e mocks


Uma coisa a se notar sobre a terminologia do Mockito é que ela confunde a distinção entre um esboço e um objeto
simulado. No Mockito, criamos duplos de teste que são rotulados como objetos simulados. Mas em nosso teste, podemos
usar essas duplas como um esboço, uma simulação ou até mesmo uma mistura de ambos.

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.

Correspondentes de argumentos – personalizando o comportamento de duplicatas de teste

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

Trabalhando com Mockito – uma biblioteca de simulação popular 143

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;

importar estático org.assertj.core.api.Assertions.assertThat; importar estático


org.mockito.ArgumentMatchers.any; importar estático org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)

classe pública UserGreetingTest {


@Zombar

perfis privados de UserProfiles;

@Teste

void formatosGreetingWithName() {
quando(perfis.fetchNicknameFor(qualquer()))
.thenReturn("Alan");

var saudação = new UserGreeting(perfis);

String real =
saudação.formatGreeting(new UserId(""));

afirmarIsso(real)

.isEqualTo("Olá e seja bem-vindo, Alan");


}
}
Machine Translated by Google

144 Duplas de teste – Stubs e simulações

Adicionamos uma correspondência de argumento any() ao stub do método fetchNicknameFor() .


Isso instrui Mockito a retornar o valor esperado Alan , independentemente do valor do parâmetro
passado para fetchNicknameFor(). Isso é útil ao escrever testes para orientar nossos leitores e ajudá-
los a entender o que é importante e o que não é para um teste específico.

Mockito oferece vários matchers de argumentos, descritos na documentação oficial do Mockito.


Essas correspondências de argumentos são especialmente úteis ao criar um stub para simular uma condição de erro.
Este é o assunto da próxima seção.

Conduzindo código de tratamento de erros com testes

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

public void rejeitaInvalidEmailRecipient() {


doThrow(nova IllegalArgumentException())
.quando(mailServer).sendEmail(any(),any(),any());

var notificações

= novo UserNotifications(mailServer);

assertThatExceptionOfType(NotificationFailureException.
aula)
.isThrownBy(()->notificações
Machine Translated by Google

Testando uma condição de erro no Wordz 145

.welcomeNewUser("não é um endereço de e-mail"));


}

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.

Para este exemplo, decidimos encapsular o SUT e relançar IllegalArgumentException.


Optamos por criar uma nova exceção que diz respeito à responsabilidade das notificações
dos usuários. Chamaremos isso de NotificationFailureException. A etapa de asserção do
teste usa o recurso da biblioteca AssertJ assertThatExceptionOfType(). Isso executa as
etapas Agir e Afirmar juntas. Chamamos nosso método SUT welcomeNewUser() e afirmamos
que ele gera nosso erro NotificationFailureException .

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.

Testando uma condição de erro no Wordz


Nesta seção, aplicaremos o que aprendemos escrevendo um teste para uma classe que escolherá uma palavra aleatória
para o jogador adivinhar, a partir de um conjunto armazenado de palavras. Criaremos uma interface chamada WordRepository
para acessar palavras armazenadas. Faremos isso através de um método fetchWordByNumber(wordNumber) , onde
wordNumber identifica uma palavra. A decisão de design aqui é que cada palavra seja armazenada com um número
sequencial começando em 1 para nos ajudar a escolher um aleatoriamente.

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

146 Duplas de teste – Stubs e simulações

Podemos escrever o teste da seguinte forma:

@ExtendWith(MockitoExtension.class)

classe pública WordSelectionTest {

@Zombar

repositório privado do WordRepository;

@Zombar

números aleatórios privados aleatórios;

@Teste

relatórios public voidWordNotFound() {


doThrow(nova WordRepositoryException())

.quando(repositório)

.fetchWordByNumber(anyInt());

var seleção = new WordSelection(repositório,


aleatório);

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.

Essas técnicas expandiram nosso kit de ferramentas para escrever testes.

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

148 Duplas de teste – Stubs e simulações

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.

2. Qual é o problema conhecido como “testar o mock”?

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

agrega valor e deve ser removido.

3. Os testes duplos podem ser usados em qualquer lugar?

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

necessários. (Veja a lista de leituras adicionais.)

Leitura adicional
• https://site.mockito.org/

Página inicial da biblioteca Mockito

• Trabalhando Efetivamente com Código Legado, Michael C. Feathers ISBN 978-0131177055

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.

Neste capítulo, vamos cobrir os seguintes tópicos principais:

• Por que os sistemas externos são difíceis

• Inversão de dependência para o resgate

• Abstraindo o sistema externo

• Escrever o código de domínio

• Substituição de duplicatas de teste por sistemas externos

• Teste unitário de unidades maiores

• Wordz – abstraindo o banco de dados


Machine Translated by Google

150 Arquitetura Hexagonal – Desacoplamento de Sistemas Externos

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.

Por que os sistemas externos são difíceis


Nesta seção, revisaremos a força motriz por trás da abordagem da arquitetura hexagonal – a dificuldade de trabalhar com
sistemas externos. Dependências de sistemas externos causam problemas no desenvolvimento. A solução leva a uma boa
abordagem de design.

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:

Figura 9.1 – Um pedaço de código faz tudo

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

Por que os sistemas externos são difíceis 151

Problemas ambientais trazem problemas


O ambiente em que nosso software é executado geralmente causa desafios. Suponha que nosso código leia dados
de um banco de dados. Mesmo que o código esteja correto, ele pode não conseguir ler esses dados, devido a
problemas no ambiente fora do nosso controle. Esses problemas incluem o seguinte:

• 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,

talvez devido à falta de índices.

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

serão quaisquer nomes adicionados por usuários reais.

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.

Acionar acidentalmente transações reais de testes


Quando nosso código está limitado a acessar apenas um sistema de produção, toda vez que usarmos esse código, algo acontecerá na produção.

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

152 Arquitetura Hexagonal – Desacoplamento de Sistemas Externos

Alerta de falso ataque com mísseis no Havaí

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.

Que dados devemos esperar?

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.

Chamadas do sistema operacional e hora do sistema

À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.

Desafios com serviços de terceiros

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

Inversão de dependência para o resgate 153

• 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.

Inversão de dependência para o resgate


Nesta seção, revisaremos uma abordagem de projeto conhecida como arquitetura hexagonal, baseada nos princípios SOLID
que já conhecemos. Usar essa abordagem nos permite usar o TDD de maneira mais eficaz em uma parte maior da nossa
base de código.

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:

Figura 9.2 – Aplicando SOLID ao nosso relatório de vendas

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

• Cálculo do total de vendas

• Lendo os dados de vendas do banco de dados


Machine Translated by Google

154 Arquitetura Hexagonal – Desacoplamento de Sistemas Externos

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 aplicar o Princípio da Inversão de Dependência aqui também. Ao inverter as dependências da


formatação e do código de acesso ao banco de dados, nosso total de vendas calculado agora fica livre de
conhecer qualquer detalhe. Fizemos um avanço significativo:

• O código de cálculo agora está totalmente isolado do banco de dados e da formatação

• Podemos trocar qualquer trecho de código que possa acessar qualquer banco de dados

• Podemos trocar qualquer trecho de código que possa formatar um relatório

• 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.

Generalizando esta abordagem para a arquitetura hexagonal


Esta combinação do princípio da responsabilidade única e da inversão de dependência parece ter- nos trazido alguns benefícios.
Poderíamos estender essa abordagem para todo o aplicativo e obter os mesmos benefícios? Poderíamos encontrar uma maneira de
separar toda a nossa lógica de aplicação e representações de dados das restrições da influência externa? Certamente podemos, e a
forma geral deste design é mostrada no diagrama a seguir:
Machine Translated by Google

Inversão de dependência para o resgate 155

Figura 9.3 – Arquitetura hexagonal

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.

Visão geral dos componentes da arquitetura hexagonal


Para nos fornecer esse isolamento da nossa lógica principal de aplicação, a arquitetura hexagonal divide todo o
programa em quatro espaços:

• Sistemas externos, incluindo navegadores da web, bancos de dados e outros serviços de computação

• Os adaptadores implementam as APIs específicas exigidas pelos sistemas externos

• As portas são a abstração do que nosso aplicativo precisa do sistema externo

• 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

156 Arquitetura Hexagonal – Desacoplamento de Sistemas Externos

Sistemas externos se conectam a adaptadores

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:

Figura 9.4 – Navegador conectado a 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

Inversão de dependência para o resgate 157

Adaptadores se conectam às portas

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:

Figura 9.5 – Adaptadores conectados às portas

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;

Comandos de interface pública {


SalesReport calculaForPeriod(LocalDate início,
Fim da DataLocal);
}

Este fragmento de código apresenta o seguinte:

• Nenhuma referência a HttpServletRequest ou qualquer coisa relacionada a HTTP

• Sem referências a formatos JSON


Machine Translated by Google

158 Arquitetura Hexagonal – Desacoplamento de Sistemas Externos

• Referências ao nosso modelo de domínio – SalesReport e 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ê.

As portas se conectam ao nosso modelo de domínio

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:

Figura 9.6 – Portas conectadas ao modelo de domínio

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

Inversão de dependência para o resgate 159

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 .

A regra de ouro – o domínio nunca se conecta diretamente aos adaptadores


Para preservar os benefícios de isolar o modelo de domínio de adaptadores e sistemas externos, seguimos uma regra
simples: o modelo de domínio nunca se conecta diretamente a nenhum dos adaptadores. Isso sempre é feito através de
uma porta.

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:

• O modelo de domínio reside em um pacote de domínio (e subpacotes)

• Os adaptadores residem em um pacote de adaptadores (e subpacotes)

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.

As regras de ouro da arquitetura hexagonal

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

160 Arquitetura Hexagonal – Desacoplamento de Sistemas Externos

Por que o formato hexágono?

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.

Abstraindo o sistema externo


Nesta seção, consideraremos algumas das decisões que precisamos tomar ao aplicar a abordagem da arquitetura hexagonal.
Adotaremos uma abordagem passo a passo para lidar com sistemas externos, onde primeiro decidiremos o que o modelo de domínio
precisa e, em seguida, elaboraremos as abstrações corretas que ocultam seus detalhes técnicos. Consideraremos dois sistemas
externos comuns: solicitações da web e acesso ao banco de dados.

Decidindo o que nosso modelo de domínio precisa

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.

Vejamos maneiras de abstrair esses sistemas externos comuns.


Machine Translated by Google

Abstraindo o sistema externo 161

Abstraindo solicitações e respostas da web

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 pública RequestSalesReport {


início localDate final privado;
final privado LocalDate final;

public RequestSalesReport(LocalDate início,


Fim da data local){
isto.start = início;
este.fim = fim;
}

public SalesReport produzir(relatório SalesReporting) {


retornar reporting.reportForPeriod(início, fim);
}
}

Aqui, podemos ver as peças críticas do nosso modelo de domínio da solicitação:

• O que estamos solicitando – ou seja, um relatório de vendas, capturado no nome da

classe • Os parâmetros dessa solicitação – ou seja, as datas de início e término do período do relatório

Podemos ver como a resposta é representada:

• A classe SalesReport conterá as informações brutas solicitadas

Também podemos ver o que não está presente:

• Os formatos de dados usados na solicitação web

• Códigos de status HTTP, como 200 OK

• HTTPServletRequest e HttpServletResponse ou objetos de estrutura equivalentes


Machine Translated by Google

162 Arquitetura Hexagonal – Desacoplamento de Sistemas Externos

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.

Abstraindo o banco de dados

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.

Uma porta de banco de dados possui dois componentes:

• Uma interface para inverter a dependência do banco de dados.

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.

• Objetos de valor que representam os próprios dados, em termos de modelo de domínio.

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;

interface pública SalesRepository {

List<Venda> allWithinDateRange(LocalDate start,


Fim da DataLocal);

}
Machine Translated by Google

Abstraindo o sistema externo 163

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.

Novamente, podemos ver o que não está presente:

• Sequências de conexão do banco de dados

• Detalhes da API JDBC ou JPA – a biblioteca padrão de conectividade de banco de dados Java

• Consultas SQL (ou consultas NoSQL)

• Esquema de banco de dados e nomes de tabelas

• Detalhes do procedimento armazenado no banco de dados

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.

Abstraindo chamadas para serviços da web

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;

interface pública MappingService {


Machine Translated by Google

164 Arquitetura Hexagonal – Desacoplamento de Sistemas Externos

void addReview(localização geográfica,


Revisão de revisão);

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.

Escrevendo o código de domínio


Nesta seção, veremos algumas coisas em que precisamos pensar enquanto escrevemos o código
para nosso modelo de domínio. Abordaremos quais tipos de bibliotecas devemos ou não usar no domínio
modelo, como lidamos com a configuração e inicialização de aplicativos e também pensaremos sobre o impacto
que os frameworks populares têm.

Decidindo o que deveria estar em nosso 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

Escrevendo o código de domínio 165

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.

Usando bibliotecas e estruturas 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.

Decidindo sobre uma abordagem de programação

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

166 Arquitetura Hexagonal – Desacoplamento de Sistemas Externos

Substituindo duplicatas de teste por sistemas externos


Nesta seção discutiremos uma das maiores vantagens que a arquitetura hexagonal traz ao TDD: alta
testabilidade. Também traz algumas vantagens de fluxo de trabalho.

Substituindo os adaptadores por testes duplos


A principal vantagem que a arquitetura hexagonal traz ao TDD é que é trivialmente fácil substituir todos os
adaptadores por testes duplos, dando-nos a capacidade de testar todo o modelo de domínio com os FIRST testes de unidade.
Podemos testar toda a lógica central do aplicativo sem ambientes de teste, bancos de dados de teste ou ferramentas HTTP como
Postman ou curl – apenas testes de unidade rápidos e repetíveis. Nossa configuração de teste é semelhante a esta:

Figura 9.7 – Testando o modelo de domínio

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.

Ganhamos vários benefícios fazendo isso:

• 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

Unidade testando unidades maiores 167

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.

Unidade testando unidades maiores

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.

Teste de unidade de histórias de usuários inteiras

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:

• A essência das solicitações dos usuários

• A essência de uma resposta da nossa aplicação

• A essência de como os dados precisam ser armazenados e acessados

• Tudo usando código livre de tecnologia

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

168 Arquitetura Hexagonal – Desacoplamento de Sistemas Externos

Testes mais rápidos e confiáveis

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.

Agora podemos testar três granularidades em nosso modelo de domínio:

• Contra um único método ou função

• Contra os comportamentos públicos de uma classe e de quaisquer colaboradores que ela tenha

• Contra a lógica central de toda uma história de usuário

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

de uma palavra para nossos usuários adivinharem.

Wordz – abstraindo o banco de dados


Nesta seção, aplicaremos o que aprendemos ao nosso aplicativo Wordz e criaremos uma porta adequada para buscar as palavras para

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.

Projetando a interface do repositório


A primeira tarefa ao projetar nosso porto é decidir o que ele deveria fazer. Para uma porta de banco de dados, precisamos pensar na divisão
entre o que queremos que nosso modelo de domínio seja responsável e o que enviaremos ao banco de dados. As portas que usamos para

um banco de dados são geralmente chamadas de interfaces de repositório.


Machine Translated by Google

Wordz – abstraindo o banco de dados 169

Três princípios gerais devem nos guiar:

• 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 banco de dados escolher uma palavra aleatoriamente

• 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

170 Arquitetura Hexagonal – Desacoplamento de Sistemas Externos

importar org.mockito.Mock;

importar org.mockito.MockitoAnnotations;

importar static org.assertj.core.api.Assertions.*;

importar estático org.mockito.Mockito.when;

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)

classe pública WordSelectionTest {

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:

final estático privado int HIGHEST_WORD_NUMBER = 3;

privado estático final int WORD_NUMBER_SHINE = 2;

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

repositório privado do WordRepository;

@Zombar

números aleatórios privados aleatórios;

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.

Chamaremos o método de teste de selectsWordAtRandom(). Queremos eliminar uma classe que


chamaremos de WordSelection e torná-la responsável por escolher uma palavra aleatoriamente no WordRepository:

@Teste

void selectsWordAtRandom() {
quando(repositório.highestWordNumber())

.thenReturn(HIGHEST_WORD_NUMBER);
Machine Translated by Google

Wordz – abstraindo o banco de dados 171

quando(repositório.fetchWordByNumber(WORD_NUMBER_SHINE))
.thenReturn("BRILHO");

quando(random.next(HIGHEST_WORD_NUMBER))
.entãoReturn(WORD_NUMBER_SHINE);

var seletor = new WordSelection(repositório,


aleatório);

String real = selector.chooseRandomWord();

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 construtor WordSelection utiliza duas dependências:

WordRepository é a porta para palavras armazenadas

RandomNumbers é a porta para geração de números aleatórios

• O método ChooseRandomWord() retornará uma palavra escolhida aleatoriamente como uma String

• A seção de organização é movida para o método beforeEachTest() :

@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

172 Arquitetura Hexagonal – Desacoplamento de Sistemas Externos

• Desse código de teste flui a seguinte definição de dois métodos de interface:

pacote com.wordz.domain;

interface pública WordRepository {


String fetchWordByNumber(int número);
int maiorNúmeroPalavra();
}

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

• Um métodohighWordNumber() para dizer qual será o número de palavra mais alto

O teste também eliminou a interface necessária para nosso gerador de números aleatórios:

pacote com.wordz.domain;

interface pública Números Aleatórios {


int próximo(int superiorBoundInclusive);
}

O método next() único retorna int no intervalo de 1 até o número upperBoundInclusive .


Com as interfaces de teste e de porta definidas, podemos escrever o código do modelo de domínio:

pacote com.wordz.domain;

public class WordSelection {repositório final


privado do WordRepository;
números aleatórios finais privados aleatórios;

public WordSelection (repositório WordRepository,


RandomNumbers aleatórios)
{ this.repository = repositório; isto.random =
aleatório;
}

public String escolhaPalavraAleatória() {


Machine Translated by Google

Resumo 173

int palavraNúmero =

random.next(repository.highestWordNumber());

retornar repositório.fetchWordByNumber (palavraNumber);


}
}

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.

Projetando o banco de dados e adaptadores de números aleatórios


O próximo trabalho é implementar a porta RandomNumbers e o código de acesso ao banco de dados que implementa nossa
interface WordRepository . Em linhas gerais, escolheremos um produto de banco de dados, pesquisaremos como conectar-se a
ele e executaremos consultas ao banco de dados e, em seguida, testaremos esse código usando um teste de integração.
Adiaremos a execução dessas tarefas para a terceira parte deste livro, no Capítulo 13, Conduzindo a camada de domínio, e no
Capítulo 14, Conduzindo a camada de banco de dados.

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

174 Arquitetura Hexagonal – Desacoplamento de Sistemas Externos

Perguntas e respostas
Dê uma olhada nas seguintes perguntas e respostas sobre o conteúdo deste capítulo:

1. Podemos adicionar a arquitetura hexagonal mais tarde?

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.

2. A arquitetura hexagonal é específica para OOP?

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.

3. Quando não devemos utilizar a arquitetura hexagonal?

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.

4. Podemos ter apenas uma porta para um sistema externo?

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:

• Arquitetura hexagonal, Alastair Cockburn: https://alistair.cockburn.us/


arquitetura hexagonal/

A descrição original da arquitetura hexagonal em termos de portas e adaptadores.

• 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.

Neste capítulo, vamos cobrir os seguintes tópicos principais:

• A pirâmide de teste

• Testes unitários – PRIMEIROS testes

• Testes de integração

• Testes ponta a ponta e de aceitação do usuário

• Pipelines CI/CD e ambientes de teste

• Wordz – teste de integração para nosso banco de dados

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

176 PRIMEIROS Testes e a Pirâmide de Testes

Para instalar o Postgres, faça o seguinte:

1. Acesse https://www.postgresql.org/download/ em seu navegador.

2. Clique no instalador correto para o seu sistema operacional:

Figura 10.1 – Seleção do instalador Postgres

3. Siga as instruções do seu sistema operacional.

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.

A pirâmide de teste em forma gráfica é a seguinte:

Figura 10.2 – A pirâmide de teste


Machine Translated by Google

A pirâmide de teste 177

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.

• Testes ponta a ponta

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.

• Testes de aceitação do usuário

É 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:

• Corra o mais rápido possível

• Cubra o máximo de código possível


Machine Translated by Google

178 PRIMEIROS Testes e a Pirâmide de Testes

• Evite tantos defeitos quanto possível

• Minimizar a duplicação do esforço de teste

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.

Testes unitários – PRIMEIROS testes

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.

Os testes unitários apresentam vantagens e limitações, conforme resumido na tabela a seguir:

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.

Tabela 10.1 – Vantagens e desvantagens dos testes unitários

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

Testes de integração 179

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.

A posição de teste ao usar apenas testes de unidade é mostrada no diagrama a seguir:

Figura 10.3 – Testes unitários cobrem o modelo de domínio

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

180 PRIMEIROS Testes e a Pirâmide de Testes

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

software conforme ele será usado ao vivo

Suscetível a problemas no ambiente de teste,


como dados incorretos ou falhas de conexão de rede

Tabela 10.2 – Vantagens e desvantagens do teste de integração

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.

O que um teste de integração deve cobrir?


Em nosso projeto até agora, desacoplamos sistemas externos de nosso código de domínio usando o Princípio de Inversão de
Dependência. Criamos uma interface que define como usamos esse sistema externo. Haverá alguma implementação desta
interface, que é o que nosso teste de integração irá cobrir. Em termos de arquitetura hexagonal, este é um adaptador.

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

Testes de integração 181

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.

Podemos representar o escopo de um teste de integração da seguinte forma:

Figura 10.4 – Testes de integração cobrem a camada adaptadora

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.

Testando adaptadores de banco de dados

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

182 PRIMEIROS Testes e a Pirâmide de Testes

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.

• Use dados aleatórios

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.

Ferramentas como o database-rider, disponível em https://database-rider.github.io/getting start/, auxiliam


fornecendo código de biblioteca para conectar-se a bancos de dados e inicializá-los com dados de teste.

Testando serviços da web

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

Testes de integração 183

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.

Teste de contrato orientado ao consumidor


Uma abordagem útil para testar interações com serviços da Web é chamada de teste de contrato orientado ao consumidor.
Consideramos nosso código como tendo um contrato com o serviço externo. Concordamos em chamar determinadas
funções da API no serviço externo, fornecendo dados na forma exigida. Precisamos que o serviço externo nos responda
de forma previsível, com dados num formato conhecido e códigos de estado bem compreendidos. Isto forma um contrato
entre as duas partes – nosso código e a API do serviço externo.

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:

Figura 10.5 – Testes de contratos orientados ao consumidor

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

184 PRIMEIROS Testes e a Pirâmide de Testes

Um teste de contrato típico precisará de dois trechos de código:

• 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.

Testes ponta a ponta e de aceitação do usuário


Nesta seção, avançaremos para o topo da pirâmide de teste. Analisaremos o que são testes de aceitação do usuário e de
ponta a ponta e o que eles acrescentam aos testes de unidade e integração.

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.

Esses testes apresentam vantagens e limitações, conforme resumido na tabela a seguir:


Machine Translated by Google

Testes ponta a ponta e de aceitação do usuário 185

Vantagens Limitações

Testes mais lentos para serem executados.


Testes de funcionalidade mais abrangentes disponíveis.
Estamos testando no mesmo nível que um usuário de
nosso sistema – seja pessoa ou máquina –
experimentaria nosso sistema.

Os testes neste nível preocupam-se com o Problemas de confiabilidade – muitos problemas na


comportamento puro observado de fora do sistema.
configuração e no ambiente do nosso sistema podem
Poderíamos refatorar e reestruturar grandes partes do causar falhas nos testes falsos negativos . Isso é chamado

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.

Tabela 10.3 – Vantagens e desvantagens do teste ponta a ponta

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

186 PRIMEIROS Testes e a Pirâmide de Testes

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.

Ferramentas de teste de aceitação

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.

Aqui estão algumas ferramentas populares de teste de aceitação a serem consideradas:

• Descanse Fácil

Uma ferramenta popular para testar APIs REST: https://resteasy.dev/


• RestAssegurado

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

Disponível em https://cucumber.io/. Pepino permite descrições semelhantes ao idioma inglês


de testes a serem escritos por especialistas no domínio. Pelo menos, essa é a teoria. Nunca vi ninguém além de
um desenvolvedor escrever testes do Cucumber em qualquer projeto do qual participei.
Machine Translated by Google

Pipelines CI/CD e ambientes de teste 187

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.

Pipelines CI/CD e ambientes de teste


Pipelines CI/CD e ambientes de teste são uma parte importante da engenharia de software. Eles fazem parte do fluxo
de trabalho de desenvolvimento que nos leva desde a escrita do código até a colocação dos sistemas nas mãos dos
usuários. Nesta seção, veremos o que os termos significam e como podemos usar essas ideias em nossos projetos.

O que é um pipeline de CI/CD?

Vamos começar definindo os termos:

• CI significa integração contínua

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.

• CD significa entrega contínua ou implantação contínua

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.

Por que precisamos de integração contínua?

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

188 PRIMEIROS Testes e a Pirâmide de Testes

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 figura a seguir mostra o objetivo do CI:

Figura 10.7 – Integração contínua

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

Pipelines CI/CD e ambientes de teste 189

Por que precisamos de entrega contínua?


Se CI tem como objetivo manter nossos componentes de software juntos como um todo em constante crescimento, então
CD tem como objetivo colocar esse todo nas mãos de pessoas que se preocupam com ele. A figura a seguir ilustra o CD:

Figura 10.8 – Entrega contínua

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 obtêm o valor que desejam

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.

• Obtemos feedback valioso dos usuários

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.

• Alinha a base de código e a equipe de desenvolvimento

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

190 PRIMEIROS Testes e a Pirâmide de Testes

Entrega contínua ou implantação contínua?


As definições exatas desses termos parecem variar, mas podemos pensar neles assim:

• Entrega contínua

Entregamos software às partes interessadas internas, como proprietários de produtos e engenheiros de controle de qualidade

• Implantação contínua

Entregamos software em produção e para usuários finais

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.

Pipelines práticos de CI/CD


A maioria dos projetos usa uma ferramenta de CI para lidar com as tarefas de sequenciamento. Ferramentas populares são
fornecidas por Jenkins, GitLab, CircleCI, Travis CI e Azure DevOps. Todos eles funcionam de forma semelhante, executando
estágios de construção separados sequencialmente. É daí que vem o nome pipeline – ele se assemelha a um tubo sendo
carregado em uma extremidade com o próximo estágio de construção e saindo pela outra extremidade do tubo, conforme
mostrado no diagrama a seguir:

Figura 10.9 – Estágios em um pipeline de CI

Um pipeline de CI compreende as seguintes etapas:

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

Pipelines CI/CD e ambientes de teste 191

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

mensagens de falha são relatadas.

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 e desafios de usar ambientes de teste

Os ambientes de teste oferecem vantagens e desvantagens, conforme resumido na tabela a seguir:


Machine Translated by Google

192 PRIMEIROS Testes e a Pirâmide de Testes

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 verdadeiro teste acontece quando colocamos nosso código no ar. Sempre.

Mais realista do que esboços Esforço extra para criar e manter

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.

Verifique suposições sobre sistemas externosPreocupações com a privacidade

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.

Tabela 10.4 – Vantagens e desafios dos ambientes de teste

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

Pipelines CI/CD e ambientes de teste 193

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:

Figura 10.10 – Implantação azul-verde

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

194 PRIMEIROS Testes e a Pirâmide de Testes

Figura 10.11 – Particionamento de tráfego

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.

Wordz – teste de integração para nosso banco de dados

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.

Buscando uma palavra do banco de dados


Como parte do nosso trabalho de design anterior, identificamos que o Wordz precisaria de um local para
armazenar as palavras candidatas a serem adivinhadas. Definimos uma interface chamada WordRepository
para nos isolar dos detalhes de armazenamento. Nessa iteração, só definimos um método na interface:

interface pública WordRepository {


String fetchWordByNumber(int wordNumber);
}
Machine Translated by Google

Wordz – teste de integração para nosso banco de dados 195

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:

• Uma biblioteca de código aberto chamada database-rider (disponível em https://database


rider.github.io/getting-started/) como ferramenta de teste

• Postgres, um popular banco de dados relacional de código aberto, para armazenar nossos dados

Aqui está o código de teste:

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;

importar estático org.assertj.core.api.Assertions.assertThat;

@DBRider

classe pública WordRepositoryPostgresTest {


fonte de dados privada ; fonte de dados privada ;

@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

196 PRIMEIROS Testes e a Pirâmide de Testes

final privado ConnectionHolder connectionHolder = () ->


dataSource.getConnection();

@Teste

@DataSet("adaptadores/data/wordTable.json")
public void buscaPalavra() {
var adaptador = novo WordRepositoryPostgres(dataSource);

String real = adaptador.fetchWordByNumber(27);

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.

2. A etapa Act chama o método fetchWordByNumber() , passando o wordNumber numérico


queremos testar. Este número está alinhado com o conteúdo do arquivo wordTable.json .

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:

1. Por que a pirâmide de teste é representada em forma de pirâmide?

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.

2. Quais são as vantagens e desvantagens entre testes unitários, de integração e de aceitação?

Testes unitários: rápidos e repetíveis. Não teste conexões com sistemas externos.
Machine Translated by Google

198 PRIMEIROS Testes e a Pirâmide de Testes

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.

3. A pirâmide de testes garante correção?

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.

4. A pirâmide de testes se aplica apenas à programação orientada a objetos?

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:

• Introdução aos testes de contratos orientados ao consumidor

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.

• Biblioteca de testes de banco de dados com base em banco de dados

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/.

• Engenharia de Software Moderna, Dave Farley, ISBN 978-0137314911

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

Detalhes sobre o que é necessário para o CD: https://minimumcd.org/minimumcd/.


Machine Translated by Google

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.

Neste capítulo, vamos cobrir os seguintes tópicos principais:

• TDD – seu lugar no panorama geral da qualidade

• Testes exploratórios manuais – descobrindo o inesperado

• Revisão de código e programação em conjunto

• Interface do usuário e testes de experiência do usuário

• Testes de segurança e monitoramento de operações

• Incorporação de elementos manuais em fluxos de trabalho de CI/CD

TDD – seu lugar no panorama geral da qualidade


Nesta seção, daremos uma olhada crítica no que o TDD trouxe para a mesa de testes e no que resta das atividades
humanas. Embora o TDD sem dúvida tenha vantagens como parte de uma estratégia de teste, ele nunca poderá ser a
estratégia completa para um sistema de software bem-sucedido.
Machine Translated by Google

200 Explorando TDD com Garantia de Qualidade

Compreendendo os limites do TDD

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.

Não há mais necessidade de testes manuais?

É 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

Exploratório manual – descobrindo o inesperado 201

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.

O TDD também nos fornece testes de regressão automatizados, gratuitamente:

Figura 11.1 – Teste de regressã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.

Exploratório manual – descobrindo o inesperado


Nesta seção, apreciaremos o papel dos testes exploratórios manuais como uma importante linha de defesa contra defeitos onde o
TDD é usado.

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

202 Explorando TDD com Garantia de Qualidade

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:

classe pública RestrictedSalesTest {


@Teste

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.

Os testes automatizados não podem fazer duas coisas:

• Pergunte a uma parte interessada o que ela deseja que o software faça

• Detectar um teste ausente

É 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

Revisão de código e programação em conjunto 203

Teste Automatizado Teste Exploratório Manual

Repetivel Criativo

Testes para resultados conhecidos Encontra resultados desconhecidos

Possível por máquina Requer criatividade humana


Verificação de comportamento Investigação de comportamento

Planejado Oportunista

O código está no controle dos testes As mentes humanas controlam os testes

Tabela 11.1 – Teste exploratório automatizado versus teste exploratório manual

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.

Revisão de código e programação em conjunto


Esta seção analisa outra área surpreendentemente resistente à automação: a verificação da qualidade do código.

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

204 Explorando TDD com Garantia de Qualidade

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.

Imediatamente, ferramentas como essa alertam sobre o seguinte:

• Convenções de nomes de variáveis não seguidas

• Variáveis não inicializadas levando a possíveis problemas de NullPointerException

• Vulnerabilidades de segurança

• Uso inadequado ou arriscado de construções de programação

• Violações de práticas e padrões aceitos pela comunidade

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:

Análise Automatizada Revisão Humana

Regras rígidas (por exemplo, comprimento de nome variável) Relaxa as regras com base no contexto

Aplica um conjunto fixo de critérios de avaliação Aplica aprendizagem experiencial

Relata resultados de aprovação/reprovação Sugere melhorias alternativas

Tabela 11.2 – Análise automatizada versus revisão humana

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:

• Revisão de código em pull requests:

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

Teste de interface do usuário e experiência do usuário 205

• 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.

• Programação de conjunto (mob):

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.

Teste de interface do usuário e experiência do usuário


Nesta seção, consideraremos como avaliamos o impacto de nossa interface de usuário nos usuários. Esta é outra área onde a
automação traz benefícios, mas não pode completar o trabalho sem o envolvimento humano.

Testando a interface do usuário


As interfaces de usuário são a única parte do nosso sistema de software que é importante para as pessoas mais importantes
de todas: nossos usuários. Eles são – literalmente – as suas janelas para o nosso mundo. Quer tenhamos uma interface de
linha de comando, um aplicativo da web móvel ou uma GUI de desktop, nossos usuários serão ajudados ou prejudicados em
suas tarefas por nossa interface de usuário.

O sucesso de uma interface de usuário depende de duas coisas bem feitas:

• Fornece todas as funcionalidades que um usuário precisa (e deseja)

• 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

206 Explorando TDD com Garantia de Qualidade

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:

Figura 11.2 – Exemplo de interface de usuário

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?

• Está alinhado com a marca corporativa e os guias de estilo da casa?

• Para a tarefa de comprar uma camiseta, ela é fácil de usar?

• Apresenta um fluxo lógico ao usuário, orientando-o em sua tarefa?

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

Teste de interface do usuário e experiência do usuário 207

Avaliando a experiência do usuário

Intimamente relacionado ao design da interface do usuário está o design da experiência do usuário.

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:

Experiência Classificação de 1 (ruim) a 5 (bom) comentários

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

pontuação mais baixa.

Tabela 11.3 – Formulário de feedback da experiência do usuário

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

208 Explorando TDD com Garantia de Qualidade

Testes de segurança e monitoramento de operações


Esta seção reflete sobre os aspectos críticos das preocupações de segurança e operações.

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.

Os riscos de segurança podem ser resumidos aproximadamente da seguinte forma:

• Coisas que não deveríamos ver

• Coisas que não deveríamos mudar

• Coisas que não deveríamos usar com tanta frequência

• Coisas sobre as quais não deveríamos mentir

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

Incorporando elementos manuais em fluxos de trabalho de CI/CD 209

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.

Incorporando elementos manuais em fluxos de trabalho de CI/CD


Vimos que os processos manuais não são apenas importantes em nosso fluxo de trabalho geral, mas, para algumas
coisas, eles são insubstituíveis. Mas como as etapas manuais se enquadram em fluxos de trabalho altamente
automatizados? Esse é o desafio que abordaremos nesta 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:

Figura 11.3 – Bloqueio de fluxo de trabalho

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:

Figura 11.4 – Fluxo de trabalho de trilha dupla


Machine Translated by Google

210 Explorando TDD com Garantia de Qualidade

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 211

Perguntas e respostas
A seguir estão algumas perguntas e respostas sobre o conteúdo deste capítulo:

1. Os pipelines TDD e CI/CD eliminaram a necessidade de testes manuais?

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.

2. A inteligência artificial (IA) automatizará as tarefas restantes?

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.

• Explore, Elizabeth Hendrickson, ISBN 978-1937785024.

• 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.

Neste capítulo vamos cobrir os seguintes tópicos principais:

• Adicionar testes primeiro

• Sempre podemos testar depois, certo?

• Testes? Eles são para pessoas que não sabem escrever código!

• Testando de dentro para fora

• Testes de fora para dentro

• Definindo limites de teste com arquitetura hexagonal

Adicionando testes primeiro

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

214 Teste primeiro, teste depois, teste nunca

Test-first é uma ferramenta de design

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:

Figura 12.1 – Projeto de primeiros socorros para testes

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:

• Quais dados de configuração serão necessários?

• Que conexões com outros objetos ou funções serão necessárias?

• Que comportamento esse código deve fornecer?

• Que insumos extras são necessários para proporcionar esse comportamento?

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:

• O nome do método deve descrever o resultado da chamada do método.

• 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

Adicionando testes primeiro 215

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.

Testes formam especificações executáveis


Outra vantagem do código de teste é que ele pode formar uma forma de documentação altamente precisa e repetível.
Simplicidade e clareza no código de teste são necessárias para conseguir isso. Em vez de escrever um documento de
planejamento de testes, escrevemos testes TDD como código, que pode ser executado por um computador. Isso tem a vantagem
de ser mais imediato para os desenvolvedores. Essas especificações executáveis são capturadas junto com o código de produção
que testam, armazenadas no controle de origem e disponibilizadas continuamente para toda a equipe.

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.

Test-first fornece métricas significativas de cobertura de código


Escrever um teste antes de escrever o código de produção dá a cada teste um propósito específico. O teste existe para eliminar
um comportamento específico em nosso código. Assim que passarmos neste teste, podemos executar o conjunto de testes
usando uma ferramenta de cobertura de código, que produzirá um relatório semelhante ao seguinte:
Machine Translated by Google

216 Teste primeiro, teste depois, teste nunca

Figura 12.2 – Relatório de cobertura de código

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.

Cuidado ao tornar uma métrica de cobertura de código um alvo

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:

classe pública WordTest {


@Teste

public void oneCorrectLetter() {


var palavra = new Palavra("A");
var pontuação = word.guess("A");
Machine Translated by Google

Adicionando testes primeiro 217

// 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.

Cuidado ao escrever todos os testes antecipadamente

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

218 Teste primeiro, teste depois, teste nunca

Escrever testes primeiro ajuda na entrega contínua


Talvez o maior benefício de escrever testes primeiro esteja em situações de entrega contínua. A entrega contínua depende de
um pipeline altamente automatizado. Depois que uma alteração de código é enviada para o controle de origem, o pipeline de
construção é iniciado, todos os testes são executados e, finalmente, ocorre uma implantação.

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.

A próxima seção analisa as vantagens e limitações da abordagem testar mais tarde.

Sempre podemos testar mais tarde, certo?


Uma abordagem alternativa para escrever testes antes do código é escrever o código primeiro e depois escrever os testes.
Esta seção compara e contrasta a escrita de testes após o código com a escrita de testes antes do código.

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:

Figura 12.3 – Fluxo de trabalho teste após

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:

“…os resultados da análise de código estático foram estatisticamente significativos


a favor do TDD. Além disso, os resultados da pesquisa revelaram que a maioria dos
desenvolvedores no experimento prefere TLD a TDD, dado o menor nível exigido de curva de
aprendizado.”

(Fonte: https://dl.acm.org/doi/10.1145/2601248.2601267)
Machine Translated by Google

Sempre podemos testar mais tarde, certo? 219

No entanto, um comentarista apontou que nesta pesquisa, o seguinte se aplica:

“…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.

Testar mais tarde é mais fácil para um iniciante em TDD

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

220 Teste primeiro, teste depois, teste nunca

Figura 12.4 – Ilustrando caminhos de execução

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.

Testar mais tarde torna mais difícil influenciar o design do software


Um dos benefícios do desenvolvimento test-first é que o ciclo de feedback é muito curto. Escrevemos um teste e depois
completamos uma pequena quantidade de código de produção. Em seguida, refatoramos conforme necessário. Isso se
afasta de um design pré-planejado em estilo cascata para um design emergente. Mudamos nosso design em resposta ao
aprendizado mais sobre o problema que estamos resolvendo, à medida que resolvemos cada vez mais problemas.

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

Teste mais tarde pode nunca acontecer

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:

• Scripts de extração, transformação e carregamento (ETL) para migrações de dados:

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.

• Trabalho de interface de usuário front-end:

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.

• Scripts de infraestrutura como código:

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

222 Teste primeiro, teste depois, teste nunca

O que acontece se não testarmos durante o desenvolvimento?


Podemos pensar que não testar é uma opção, mas na realidade, os testes sempre acontecerão em algum
momento. Podemos ilustrar isso com uma linha do tempo dos possíveis pontos em que os testes podem ocorrer:

Figura 12.5 – Cronograma de teste

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.

Testando de dentro para fora


Nesta seção, revisaremos nossa escolha de ponto de partida para nossas atividades de TDD. O primeiro lugar a observar é
dentro do nosso sistema de software, começando pelos detalhes.

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

Testando de dentro para fora 223

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:

Figura 12.6 – Desenvolvimento de dentro para fora

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.

A abordagem de dentro para fora tem algumas vantagens:

• 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

há configuração para interfaces de usuário, stubs de serviços da web ou bancos de dados.

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

224 Teste primeiro, teste depois, teste nunca

• 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.

• Risco de aprisionamento na implementação: Relacionado ao ponto anterior, às vezes passamos de um projeto


inicial tendo aprendido mais sobre o problema que estamos resolvendo, mas nem sempre reconhecemos um custo
irrecuperável. Sempre existe a tentação de continuar usando um componente que escrevemos anteriormente,
mesmo que ele não se encaixe mais tão bem, só porque investimos tempo e dinheiro para criá-lo.

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.

Testando de fora para dentro


Dado que o TDD de dentro para fora tem alguns desafios e pontos fortes, que diferença faz o TDD de fora para dentro?
Esta seção analisa a abordagem alternativa de começar de fora do sistema.

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

Testando de fora para dentro 225

Podemos ilustrar a abordagem de fora para dentro da seguinte forma:

Figura 12.7 – Vista de fora para dentro

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

226 Teste primeiro, teste depois, teste nunca

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

e de fora para dentro do TDD.

Definindo limites de teste com arquitetura hexagonal


O tópico desta seção é como o uso de uma arquitetura hexagonal impacta o TDD. Saber que estamos
usando uma arquitetura hexagonal apresenta limites úteis para os diferentes tipos de testes na pirâmide de testes.

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

vantagens ao TDD. A razão está no uso de portas e adaptadores.

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.

De dentro para fora funciona bem com o modelo de domínio

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

Definindo limites de teste com arquitetura hexagonal 227

Este tipo de componente reside no modelo de domínio – o hexágono interno:

Figura 12.8 – Testando a lógica do domínio

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.

Outside-in funciona bem com adaptadores


O TDD de estilo mockista aborda o desenvolvimento de uma perspectiva de fora para dentro. Esta é uma ótima
combinação para nossa camada adaptadora em uma arquitetura hexagonal. Podemos assumir que a lógica principal do
aplicativo reside no modelo de domínio e foi testada lá com testes unitários rápidos. Isso deixa os adaptadores no
hexágono externo para serem testados por testes de integração.
Machine Translated by Google

228 Teste primeiro, teste depois, teste nunca

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:

Figura 12.9 – Testando adaptadores

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.

As histórias de usuários podem ser testadas em todo o modelo de domínio

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

Definindo limites de teste com arquitetura hexagonal 229

Figura 12.10 – Testando histórias de usuários

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

230 Teste primeiro, teste depois, teste nunca

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.

2. Como a arquitetura hexagonal afeta o TDD?

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.

3. O que acontece se abandonarmos completamente os testes?

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 231

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

• Trabalhando Efetivamente com Código Legado, Michael Feathers, ISBN 978-0131177055

• Desenvolvimento Orientado a Testes por Exemplo, Kent Beck, ISBN 978-0321146533

• 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.

Esta parte possui os seguintes capítulos:

• Capítulo 13, Conduzindo a Camada de Domínio

• Capítulo 14, Conduzindo a camada de banco de dados

• Capítulo 15, Conduzindo a Camada Web


Machine Translated by Google
Machine Translated by Google

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.

Neste capítulo, vamos cobrir os seguintes tópicos principais:

• Iniciar um novo jogo

• 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.

Iniciando um novo jogo


Nesta seção, começaremos codificando nosso jogo. Como todo projeto, começar costuma ser bastante difícil, sendo
a primeira decisão simplesmente por onde começar. Uma abordagem razoável é encontrar uma história de usuário
que comece a dar corpo à estrutura do código. Depois que tivermos uma estrutura razoável para um aplicativo, será
muito mais fácil descobrir onde o novo código deve ser adicionado.
Machine Translated by Google

236 Conduzindo a camada de domínio

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.

A primeira história de usuário a ser trabalhada é iniciar um novo jogo:

• Como jogador, quero começar um novo jogo para ter uma nova palavra para adivinhar

Quando iniciamos um novo jogo, devemos fazer o seguinte:

1. Selecione uma palavra aleatoriamente entre as palavras disponíveis para adivinhar

2. Armazene a palavra selecionada para que as pontuações das suposições possam ser calculadas

3. Registre que o jogador pode agora fazer uma estimativa inicial

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í.

Test-drive iniciando um novo jogo


Em termos gerais, usar arquitetura hexagonal significa que somos livres para usar uma abordagem de fora para
dentro com TDD. Qualquer que seja o projeto que criemos para nosso modelo de domínio, nada dele envolverá
sistemas externos difíceis de testar. Nossos testes unitários têm a garantia de serem PRIMEIROS – rápidos,
isolados, repetíveis, autoverificáveis e oportunos.

É 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;

classe pública NewGameTest {

}
Machine Translated by Google

Iniciando um novo jogo 237

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;

classe pública NewGameTest {


void iniciaNovoJogo() {
var jogo = novo jogo();
}
}

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;

classe pública Jogo {


}

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:

classe pública NewGameTest {


@Teste

void iniciaNovoJogo() {
var jogo = novo jogo();
var jogador = novo jogador();

jogo.start(jogador);
}
}
Machine Translated by Google

238 Conduzindo a camada de domínio

Permitimos que o IDE gere o método no controlador:

classe pública Jogo {


public void start(Jogador jogador) {
}

Acompanhando o progresso do jogo


As próximas decisões de design dizem respeito ao resultado esperado do início de um novo jogo para um jogador.
Há duas coisas que precisam ser registradas:

• A palavra selecionada que o jogador tenta adivinhar

• Que esperamos o primeiro palpite deles em seguida

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.

Devemos escolher uma das seguintes opções para prosseguir:

• 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:

classe pública NewGameTest {


@Teste
Machine Translated by Google

Iniciando um novo jogo 239

void iniciaNovoJogo() { var palavraz


= new Palavraz();
var jogador = novo jogador();

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:

classe pública Wordz {

public void start(Jogador jogador) { }

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:

classe pública NewGameTest {


@Teste

void iniciaNovoJogo() {
var palavraz = new Palavraz();
var jogador = novo jogador();

wordz.newGame(jogador);
}
}

O código de produção da classe Wordz também tem o método renomeado.

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.junit.jupiter.api.Test; importar


org.junit.jupiter.api.extension.ExtendWith; importar org.mockito.Mock;
Machine Translated by Google

240 Conduzindo a camada de domínio

importar org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)

classe pública NewGameTest {


@Zombar

GameRepository privado gameRepository;

@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 .

4. Permitimos que o IDE crie uma interface vazia para nós:

pacote com.wordz.domain;

interface pública GameRepository {


}

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:

classe pública NewGameTest {


@Zombar

GameRepository privado gameRepository;


@Teste

void iniciaNovoJogo() {
Machine Translated by Google

Iniciando um novo jogo 241

var jogador = novo jogador();

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.

Nota sobre o uso de getters no modelo de domínio


A classe Game possui métodos getXxx() , conhecidos como getters na terminologia Java, para cada um
de seus campos privados. Esses métodos quebram o encapsulamento de dados.

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.

Outra abordagem razoável é adicionar um método de diagnóstico getXxx() com visibilidade em


nível de pacote apenas para teste. Verifique com a equipe se isso não faz parte da API pública e não
utilize no código de produção. É mais importante acertar o código do que ficar obcecado com
curiosidades sobre design.
Machine Translated by Google

242 Conduzindo a camada de domínio

6. Criamos métodos vazios para esses novos getters usando o IDE. A próxima etapa é executar o
NewGameTest e confirmar se ele falhou:

Figura 13.1 – Nosso teste reprovado

7. Isso é o suficiente para escrevermos mais alguns códigos de produção:

pacote com.wordz.domain;

classe pública Wordz { private


final GameRepository gameRepository;

public Wordz(GameRepository gr)


{ this.gameRepository = gr;
}

public void newGame(Jogador jogador) {


var jogo = novo Jogo(jogador, "ARISE", 0);
gameRepository.create(jogo);
}
}

Podemos executar novamente o NewGameTest e vê-lo passar:


Machine Translated by Google

Iniciando um novo jogo 243

Figura 13.2 – O teste passa

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.

8. Extraia o método getGameInRepository() para maior clareza:

@Teste

void iniciaNovoJogo() {
var jogador = novo jogador();

wordz.newGame(jogador);

Jogo jogo = getGameInRepository();


assertThat(game.getWord()).isEqualTo("ARISE");
assertThat(game.getAttemptNumber()).isZero();
assertThat(game.getPlayer()).isSameAs(player);
}

jogo privado getGameInRepository() {


var gameArgumento
= ArgumentCaptor.forClass(Game.class)
verificar (gameRepositório)
.create(gameArgument.capture());
retornar gameArgument.getValue();
}
Machine Translated by Google

244 Conduzindo a camada de domínio

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.

Triangulação de seleção de palavras

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)

classe pública NewGameTest {


@Zombar

GameRepository privado gameRepository;

@Zombar

wordRepository privado wordRepository ;

@Zombar

números aleatórios privados números aleatórios;


Machine Translated by Google

Iniciando um novo jogo 245

@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 :

classe pública Wordz {


final privado GameRepository gameRepository;
privado final WordSelection wordSelection ;

public Wordz(GameRepository gr,


WordRepositório wr,
Números Aleatórios rn) {
this.gameRepository = gr;
this.wordSelection = new WordSelection(wr, rn);

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.

4. Montamos os mocks. Queremos que eles simulem o comportamento que esperamos da


interface WordRepository quando chamamos o método fetchWordByNumber() e da
interface RandomNumbers quando chamamos next():

@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

246 Conduzindo a camada de domínio

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");
}

privado void dadoWordToSelect(String wordToSelect){


int palavraNúmero = 2;

quando(randomNumbers.next(anyInt())) .thenReturn(wordNumber);

quando(palavraRepositório

.fetchWordByNumber(palavraNumber)) .thenReturn(palavraToSelect);

}
Machine Translated by Google

Iniciando um novo jogo 247

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");

var jogador = novo jogador();


wordz.newGame(jogador);

Jogo jogo = getGameInRepository();


assertThat(game.getWord()).isEqualTo("ABCDE");
}

Isso segue a mesma abordagem do teste anterior, startNewGame.

7. Observe o teste falhar. Escreva o código de produção para fazer o teste passar:

public void newGame(Jogador jogador) {


var palavra = wordSelection.chooseRandomWord();

Jogo jogo = novo Jogo(jogador, palavra, 0);


gameRepository.create(jogo);
}

8. Observe a aprovação do novo teste e execute todos os testes:

Figura 13.3 – Teste original falhando

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

248 Conduzindo a camada de domínio

adicione a configuração simulada necessária ao nosso teste original. Podemos reutilizar nosso dadoWordToSelect()
método auxiliar para fazer isso.

9. Adicione a configuração simulada ao teste original:

@Teste

void iniciaNovoJogo() {
var jogador = novo jogador();
dadoWordToSelect("ARISE");

wordz.newGame(jogador);

Jogo jogo = getGameInRepository();


assertThat(game.getWord()).isEqualTo("ARISE");
assertThat(game.getAttemptNumber()).isZero();
assertThat(game.getPlayer()).isSameAs(player);
}

10. Execute novamente todos os testes e confirme se todos foram aprovados:

Figura 13.4 – Todos os testes aprovados

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 :

1. Extraia uma constante para a variável Player :

private static final Player PLAYER = new Player();

@Teste
Machine Translated by Google

Jogando o jogo 249

void iniciaNovoJogo() {
dadoWordToSelect("ARISE");

palavraz.newGame(PLAYER);

Jogo jogo = getGameInRepository();


assertThat(game.getWord()).isEqualTo("ARISE");
assertThat(game.getAttemptNumber()).isZero();
assertThat(game.getPlayer()).isSameAs(PLAYER);
}

2. Executaremos todos os testes depois disso para garantir que todos ainda passem e que
selectsRandomWord() tenha desaparecido.

Figura 13.5 – Todos os testes aprovados

É 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

250 Conduzindo a camada de domínio

Projetando a interface de pontuação


A primeira decisão de design que devemos tomar é o que precisamos retornar após adivinhar a palavra.
Precisamos retornar as seguintes informações ao usuário:

• A pontuação do palpite atual

• Se o jogo ainda está em jogo ou terminou

• Possivelmente o histórico anterior de pontuação para cada palpite

• Possivelmente um relatório de erros de entrada do usuário

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)

classe pública AdivinhaTeste {


private static final Player PLAYER = new Player();
private static final String CORRECT_WORD = "ARISE";
string final estático privado WRONG_WORD = "RXXXX";

@Zombar

GameRepository privado gameRepository;

@InjectMocks
palavra privada Wordz;
Machine Translated by Google

Jogando o jogo 251

@Teste

void retornaScoreForGuess() {
dadoGameInRepository(

Game.create(PLAYER, CORRECT_WORD));

Resultado da estimativa = wordz.assess(PLAYER, WRONG_WORD);

Letra primeiraLetra = resultado.pontuação().letra(0);


assertThat(primeiraLetra)

.isEqualTo(Carta.PART_CORRECT);
}

private void dadoGameInRepository(Jogo jogo) { quando(gameRepository

.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;

registro público GuessResult(


Pontuação,
booleano isGameOver

){}
Machine Translated by Google

252 Conduzindo a camada de domínio

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:

public GuessResult avaliar(Player player, String palpite) {


var jogo = gameRepository.fetchForPlayer(player);
var alvo = new Word(game.getWord());
var pontuação = target.guess(palpite);
retornar novo GuessResult(pontuação, falso);
}

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.

Triangulação do acompanhamento do 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:

1. Adicionamos um teste para eliminar este código:

@Teste

void atualizaçõesAttemptNumber() {
dadoGameInRepository(
Game.create(PLAYER, CORRECT_WORD));

palavraz.avaliar(PLAYER, WRONG_WORD);

var jogo = getUpdatedGameInRepository();


assertThat(game.getAttemptNumber()).isEqualTo(1);
}

jogo privado getUpdatedGameInRepository() {


Machine Translated by Google

Jogando o jogo 253

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:

2. Adicionamos o método update() à interface GameRepository :

pacote com.wordz.domain;

interface pública GameRepository {


void create(Jogo jogo);
Jogo fetchForPlayer(Jogador); atualização nula (jogo);

3. Adicionamos o código de produção ao método assessment() na classe Wordz para


incrementar tryNumber e chamar update():

public GuessResult avaliar (Player player, String palpite) { var game =


gameRepository.fetchForPlayer (player); game.incrementAttemptNumber();
gameRepository.update(jogo);

var alvo = new Word(game.getWord());


var pontuação = target.guess(palpite);
retornar novo GuessResult(pontuação, falso);
}

4. Adicionamos o método incrementAttemptNumber() à classe Game:

public void incrementoAttemptNumber() {


tentativaNúmero++;
}
Machine Translated by Google

254 Conduzindo a camada de domínio

O teste agora passa. Podemos pensar em quaisquer melhorias de refatoração que queiramos fazer. Há
duas coisas que parecem se destacar:

• A configuração de teste duplicada entre a classe NewGameTest e a classe GuessTest.

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:

public GuessResult avaliar(Player player, String palpite) {


var jogo = gameRepository.fetchForPlayer(player);
Pontuação pontuação = game.attempt(palpite);
gameRepository.update(jogo);
retornar novo GuessResult(pontuação, falso);
}

Movemos o código que estava aqui para o método recém-criado: try() na classe
Game:

tentativa de pontuação pública (String lastGuess) {


tentativaNúmero++;
var alvo = new Word(targetWord);
retornar target.guess(latestGuess);
}

Renomear o argumento do método de palpite para LatestGuess melhora a legibilidade.

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:

• Adivinhe a palavra corretamente

• Fazer nossa tentativa final permitida, com base em um número máximo

Podemos começar codificando a detecção de fim de jogo quando adivinhamos a palavra corretamente.
Machine Translated by Google

Terminando o jogo 255

Respondendo a um palpite correto


Neste caso, o jogador adivinha a palavra alvo corretamente. O jogo termina e o jogador recebe uma série
de pontos, com base em quantas tentativas foram necessárias antes que o palpite correto fosse feito.
Precisamos comunicar que o jogo acabou e quantos pontos foram atribuídos, gerando dois novos
campos em nossa classe GuessResult. Podemos adicionar um teste à nossa classe existente GuessTest
da seguinte forma:

@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);

var palpite = "ARISE";


Resultado GuessResult = wordz.assess(jogador, palpite);

assertThat(result.isGameOver()).isTrue();

Isso elimina um novo acesso isGameOver() na classe GuessResult e o comportamento para


tornar isso verdadeiro:

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);

Isso por si só gera dois novos testes na classe WordTest:

@Teste

void relatóriosAllCorrect() {
Machine Translated by Google

256 Conduzindo a camada de domínio

var palavra = new Palavra("ARISE");


var pontuação = word.guess("ARISE");
assertThat(score.allCorrect()).isTrue();
}

@Teste

void relatóriosNotAllCorrect() {
var palavra = new Palavra("ARISE");
var pontuação = word.guess("ARI*E");
assertThat(score.allCorrect()).isFalse();
}

Eles próprios conduzem uma implementação na classe Score:

public boolean allCorrect() { var totalCorrect =


results.stream()
.filter(letra -> letra == Carta.CORRETO)
.contar();

return totalCorreto == resultados.size();


}

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.

Triangulação do jogo devido a muitas suposições incorretas

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));

Resultado da estimativa = wordz.assess(PLAYER, WRONG_WORD);


Machine Translated by Google

Terminando o jogo 257

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:

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,
game.hasNoRemainingGuesses());
}

Adicionamos este método de suporte à decisão à classe Game:

public boolean hasNoRemainingGuesses() {


return tryNumber == MAXIMUM_NUMBER_ALLOWED_GUESSES;
}

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.

Triangulação da resposta para adivinhar após o fim do jogo

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

258 Conduzindo a camada de domínio

dadoGameInRepository(gameOver);

Resultado da estimativa = wordz.assess(PLAYER, WRONG_WORD);

assertThat(result.isError()).isTrue();
}

Existem algumas decisões de design capturadas neste teste:

• Terminado o jogo, registramos isso em um novo campo, isGameOver, na classe Game.

• Este novo campo deverá ser definido sempre que o jogo terminar. Precisaremos de mais testes para dirigir
esse comportamento.

• Usaremos um mecanismo simples de relatório de erros – um novo campo, isError, na classe


AdivinharResultado.

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:

public GuessResult avaliar (Player player, String palpite) { var game =


gameRepository.fetchForPlayer (player);

if(game.isGameOver()) {
retornar GuessResult.ERROR;
}

Pontuação pontuação = game.attempt(palpite); if


(pontuação.allCorrect()) {
retornar novo GuessResult(pontuação, verdadeiro, falso);
}

gameRepository.update(jogo); retornar novo


GuessResult(pontuação,
game.hasNoRemainingGuesses(), falso);
}
Machine Translated by Google

Terminando o jogo 259

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:

público estático final GuessResult ERRO


= novo GuessResult(nulo, verdadeiro, verdadeiro);

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);

Jogo jogo = getUpdatedGameInRepository();


assertThat(game.isGameOver()).isTrue();
}

Aqui está o código de produção para adicionar essa lógica de gravação:

public GuessResult avaliar (Player player, String palpite) { var game =


gameRepository.fetchForPlayer (player);

if(game.isGameOver()) {
retornar GuessResult.ERROR;
}

Pontuação pontuação = game.attempt(palpite); if


(score.allCorrect()) { game.end();

gameRepository.update(jogo);

retornar novo GuessResult(pontuação, verdadeiro, falso);


}

gameRepository.update(jogo);
retornar novo GuessResult(pontuação,
Machine Translated by Google

260 Conduzindo a camada de domínio

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.

Revendo nosso design


Temos feito pequenas etapas táticas de refatoração à medida que escrevemos o código, o que é sempre uma boa ideia.
Tal como acontece com a jardinagem, é muito mais fácil manter o jardim arrumado se arrancarmos as ervas daninhas antes
que cresçam. Mesmo assim, vale a pena dar uma olhada holística no design do nosso código e nos testes antes de
prosseguirmos. Talvez nunca mais tenhamos a chance de tocar nesse código novamente, e ele tem nosso nome nele.
Vamos fazer disso algo de que nos orgulhemos e que seja seguro e simples para nossos colegas trabalharem no futuro.

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;

classe pública Wordz {


final privado GameRepository gameRepository;
seleção final privada de WordSelection;

public Wordz (repositório GameRepository,


WordRepositório wordRepositório,
Números aleatórios Números aleatórios) {
this.gameRepository = repositório;
esta.seleção =

novo WordSelection(wordRepository, randomNumbers);


}

public void newGame(Jogador jogador) {


var palavra = wordSelection.chooseRandomWord();
gameRepository.create(Game.create(jogador, palavra));
}
Machine Translated by Google

Terminando o jogo 261

Nosso método de avaliação () refatorado agora se parece com isto:

public GuessResult avaliar(Player player, String palpite) {


Jogo jogo = gameRepository.fetchForPlayer(jogador);

if(game.isGameOver()) {
retornar GuessResult.ERROR;

Pontuação pontuação = game.attempt(palpite);

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;

registro público GuessResult(


Pontuação,
booleano isGameOver,
booleano éError

){
ERRO de GuessResult final estático

= novo GuessResult(nulo, verdadeiro, verdadeiro);

GuessResult estático criar (pontuação,

booleano isGameOver) {
retornar novo GuessResult(pontuação, isGameOver, false);

}
}
Machine Translated by Google

262 Conduzindo a camada de domínio

Isso simplifica o método Assessment() eliminando a necessidade de entender o sinalizador booleano final:

public GuessResult avaliar(Player player, String palpite) {


Jogo jogo = gameRepository.fetchForPlayer(jogador);

if(game.isGameOver()) {
retornar GuessResult.ERROR;
}

Pontuação pontuação = game.attempt(palpite);

gameRepository.update(jogo);

retornar GuessResult.create(pontuação, game.isGameOver());


}

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);

Resultado da estimativa = wordz.assess(PLAYER, WRONG_WORD);

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:

Figura 13.6 – Relatório de cobertura de código

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

264 Conduzindo a camada de domínio

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.

2. Qual é a importância de 100% de cobertura de código ao executar nossos 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.

4. Toda essa refatoração é normal?

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

sobre como criar asserções personalizadas aqui: https://assertj.

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

Leitura adicional 265

• 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.

Neste capítulo, vamos cobrir os seguintes tópicos principais:

• Criação de um teste de integração de banco de dados

• Implementando o adaptador de repositório de palavras

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.

Instalando o banco de dados Postgres


Usaremos o banco de dados Postgres neste capítulo, que precisa de instalação. Para instalar o Postgres, siga estas
etapas:

1. Acesse a seguinte página da web: https://www.postgresql.org/download/.

2. Siga as instruções de instalação do seu sistema operacional.

O código foi testado com a versão 14.5. Espera-se que funcione em todas as versões.
Machine Translated by Google

268 Conduzindo a camada de banco de dados

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.

Criando 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

estamos trabalhando na implementação da interface WordRepository , que acessará palavras armazenadas em um


banco de dados Postgres.

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'

testRuntimeOnly 'org.junit.jupiter:junit-jupiter engine: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'

testImplementation 'com.github.database-rider:rider junit5:1.33.0'

implementação 'org.postgresql:postgresql:42.5.0'
}
Machine Translated by Google

Criando um teste de integração de banco de dados 269

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.

Criando um teste de banco de dados com DBRider

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:

1. Crie um novo arquivo de classe de teste no diretório /test/ no novo com.wordz.adapters.


pacote banco de dados :

Figura 14.1 – Teste de integração

O IDE irá gerar a classe de teste vazia para nós.

2. Adicione as anotações @DBRider e @DBUnit à classe de teste:

@DBRider

@DBUnit(caseSensitiveTableNames = verdadeiro,

caseInsensitiveStrategy= Ortografia.LOWERCASE)

classe pública WordRepositoryPostgresTest {


}

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

270 Conduzindo a camada de banco de dados

4. Execute o teste. Irá falhar:

Figura 14.2 – DBRider não consegue se conectar ao banco de dados

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

classe pública WordRepositoryPostgresTest {


fonte de dados privada; fonte de dados privada;

final privado ConnectionHolder connectionHolder


= () -> dataSource.getConnection();
}

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:

Figura 14.3 – dataSource é nulo

6. Corrigimos isso adicionando um método @BeforeEach para configurar o dataSource:

@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

Criando um teste de integração de banco de dados 271

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).

7. Execute o teste e veja se ele falha:

Figura 14.4 – Usuário não existe

O erro é causado porque ainda não temos um usuário ciuser conhecido em nosso banco de dados
Postgres . Vamos criar um.

8. Abra um terminal psql e crie o usuário:

crie o usuário ciuser com senha 'cipassword';

9. Execute o teste novamente:

Figura 14.5 – Banco de dados não encontrado

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

272 Conduzindo a camada de banco de dados

10. No terminal psql , crie o banco de dados:

criar banco de dados wordzdb;

11. Execute o teste novamente:

Figura 14.6 – Aprovações no teste

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() .

Expulsando o código de produção


Nosso objetivo é testar o código para buscar uma palavra no banco de dados. Queremos que esse código esteja
em uma classe que implemente a interface WordRepository , que definimos no modelo de domínio. Queremos
projetar nosso esquema de banco de dados suficiente para suportar isso. Ao começar a adicionar código à etapa
Assert, podemos implementar uma implementação rapidamente. Esta é uma técnica útil – escrever o teste
começando com a afirmação, para que comecemos com o resultado desejado. Podemos então trabalhar de trás
para frente para incluir tudo o que é necessário para entregá-lo:

1. Adicione a etapa Assert ao nosso teste fetchesWord() :

@Teste

public void buscaPalavra() {


String real = "";
assertThat(actual).isEqualTo("ARISE");
}

Queremos verificar se podemos buscar a palavra ARISE no banco de dados. Este teste falha. Nós precisamos

para criar uma classe para conter o código necessário.


Machine Translated by Google

Criando um teste de integração de banco de dados 273

2. Queremos que nossa nova classe adaptadora implemente a interface WordRepository , então
eliminamos isso na etapa Organizar do nosso teste:

@Teste

public void buscaPalavra() {


Repositório WordRepository
= novo WordRepositoryPostgres();

String real = "";


assertThat(actual).isEqualTo("ARISE");
}

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:

Figura 14.7 – Assistente de Nova Classe

Isso resulta em um esqueleto vazio para a classe:

pacote com.wordz.adapters.db;

importar com.wordz.domain.WordRepository;

classe pública WordRepositoryPostgres implementa


Repositório de palavras {
}
Machine Translated by Google

274 Conduzindo a camada de banco de dados

4. O IDE gerará automaticamente stubs de método para a interface:

classe pública WordRepositoryPostgres implementa


Repositório de palavras {
@Sobrepor

public String fetchWordByNumber(int número) {


retornar nulo;
}

@Sobrepor

public int maiorNúmero da palavra() {


retornar 0;
}
}

5. Voltando ao nosso teste, podemos adicionar a linha act, que chamará o método fetchWordByNumber() :

@Teste

public void buscaPalavra() {


Repositório WordRepository = new
WordRepositoryPostgres();

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.

6. Passe dataSource para o construtor WordRepositoryPostgres para que nossa classe


tem uma maneira de acessar o banco de dados:

@Teste

public void buscaPalavra() {


Repositório WordRepository
= novo
Machine Translated by Google

Criando um teste de integração de banco de dados 275

WordRepositoryPostgres(dataSource);

String real = adaptador.fetchWordByNumber(27);

assertThat(actual).isEqualTo("ARISE");
}

Isso gera uma mudança no construtor:

public WordRepositoryPostgres(DataSource dataSource){


// Não implementado
}

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"

}
]

O código "word_number": 27 aqui corresponde ao valor usado no código de teste.

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:

Figura 14.8 – Localização do wordTable.json


Machine Translated by Google

276 Conduzindo a camada de banco de dados

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 .

Implementando o adaptador WordRepository


Nesta seção, usaremos a popular biblioteca de banco de dados JDBI para implementar fetchWordByNumber()
método de interface WordRepository e fazer nosso teste de integração com falha passar.

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.

Vamos testar isso:

1. Execute o teste para revelar que a tabela de palavras não existe:

Figura 14.9 – Tabela não encontrada


Machine Translated by Google

Implementando o adaptador WordRepository 277

2. Corrija isso criando uma tabela de palavras no banco de dados. Usamos o console psql para executar o
Comando SQL para criar tabela :

criar palavra da tabela (word_number int chave primária,


palavra char(5));

3. Execute o teste novamente. O erro muda para mostrar que nosso usuário ciuser tem permissões insuficientes:

Figura 14.10 – Permissões insuficientes

4. Corrigimos isso executando o comando SQL grant no console psql :

conceder seleção, inserção, atualização, exclusão em todas as tabelas no esquema


público para ciuser;

5. Execute o teste novamente. O erro muda para nos mostrar que a palavra não foi lida do
tabela do banco de dados:

Figura 14.11 – Palavra não encontrada

Acessando o 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

278 Conduzindo a camada de banco de dados

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'

testImplementation 'com.github.database-rider:rider junit5:1.35.0'

implementação 'org.postgresql:postgresql:42.5.0' implementação


'org.jdbi:jdbi3-core:3.34.0'
}

Observação

O código em si é descrito na documentação JDBI, encontrada aqui: https://jdbi. org/#_queries.

Siga estas etapas para acessar o banco de dados:

1. Primeiro, crie um objeto jdbi no construtor da nossa classe:

classe pública WordRepositoryPostgres implementa


WordRepository {
final privado Jdbi jdbi;

public WordRepositoryPostgres(DataSource dataSource)


{ jdbi =
Jdbi.create(dataSource);
}
}

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:

String final estática privada SQL_FETCH_WORD_BY_NUMBER


= "selecione a palavra da palavra onde"

+ "palavra_número=:palavraNúmero";
Machine Translated by Google

Implementando o adaptador WordRepository 279

3. O código de acesso jdbi pode ser adicionado ao método fetchWordByNumber() :

@Sobrepor

public String fetchWordByNumber(int wordNumber) { String palavra =


jdbi.withHandle(handle -> {
var consulta =
handle.createQuery(SQL_FETCH_WORD_BY_NUMBER);
query.bind("palavraNumber", palavraNumber);

retornar query.mapTo(String.class).one();
});

palavra de retorno;

4. Execute o teste novamente:

Figura 14.12 – Aprovação no teste

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

280 Conduzindo a camada de banco de dados

No psql, digite o seguinte:

CRIAR TABELA jogo (


caractere player_name variando NOT NULL,
caractere de palavra (5),

número_de_tentativa inteiro DEFAULT 0,


is_game_over booleano DEFAULT falso
);

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.

2. Quais ferramentas podem ajudar a automatizar a criação de bancos de dados?

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.

3. Quais ferramentas podem ajudar na instalação do banco de dados?

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

Leitura adicional 281

4. Com que frequência devemos executar os testes de integração?

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

• Documentação para DBRider: https://github.com/database-rider/database


cavaleiro

• Documentação JDBI: https://jdbi.org/#_introduction_to_jdbi_3

• 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.

Neste capítulo, vamos cobrir os seguintes tópicos principais:

• Iniciar um novo 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.

Antes de tentar executar o aplicativo final, execute as seguintes etapas:

1. Certifique-se de que o banco de dados Postgres esteja sendo executado localmente.

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

284 Conduzindo a camada da Web

3. Abra o terminal de comando Postgres pqsl e digite o seguinte comando SQL:

inserir em valores de palavras (1, 'ARISE'), (2, 'SHINE'), (3,


'LIGHT'), (4, 'SLEEP'), (5, 'BEARS'), (6, 'GREET'), (7,
'GRATO');

4. Instale o Postman seguindo as instruções em https://www.postman.com/downloads/.

Iniciando um novo jogo


Nesta seção, testaremos um web adaptor que fornecerá ao nosso modelo de domínio uma API
HTTP. Clientes web externos poderão enviar solicitações HTTP a esse endpoint para acionar ações
em nosso modelo de domínio para que possamos jogar. A API retornará respostas HTTP
apropriadas, indicando a pontuação da estimativa enviada e informando quando o jogo terminar.

As seguintes bibliotecas de código aberto serão usadas para nos ajudar a escrever o código:

• Molécula: Esta é uma estrutura HTTP leve

• 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.

Adicionando bibliotecas necessárias ao projeto


Precisamos adicionar as três bibliotecas Molecule, Undertow e Gson ao arquivo build.gradle antes de
podermos usá-las:

Adicione o seguinte código ao arquivo build.gradle :

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

Iniciando um novo jogo 285

testImplementation 'com.github.database-rider:rider junit5:1.35.0'

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'
}

Escrevendo o teste com falha

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.

Como sempre, começamos criando uma classe de teste:

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;

classe pública WordzEndpointTest {


}

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.

Nosso primeiro teste será iniciar um novo jogo:


@Teste

void startJogo() {
}
Machine Translated by Google

286 Conduzindo a camada da Web

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

void startGame() lança IOException,


InterruptedException {

var httpClient = HttpClient.newHttpClient();


Resposta HttpResponse
= httpClient.send(req,
HttpResponse.BodyHandlers.discarding());

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

jogador final estático privado PLAYER


= novo Jogador("alan2112");

void startGame() lança IOException,


InterruptedException {
Machine Translated by Google

Iniciando um novo jogo 287

var req = HttpRequest.newBuilder()


.uri(URI.create("htp://localhost:8080/start"))
.POST(HttpRequest.BodyPublishers
.ofString(new Gson().toJson(PLAYER)))
.construir();

var httpClient = HttpClient.newHttpClient();


HttpResponse res =
httpClient.send(req,
HttpResponse.BodyHandlers.discarding());

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.

5. Podemos executar nosso teste de integração e confirmar que ele falhou:

Figura 15.1 – Um teste que falhou – nenhum servidor HTTP

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

288 Conduzindo a camada da Web

Criando nosso servidor HTTP


O teste com falha nos permite testar o código que implementa um servidor HTTP. Usaremos a biblioteca
Molecule para nos fornecer serviços HTTP:

1. Adicione uma classe de endpoint, que chamaremos de classe WordzEndpoint:

@Teste

void startGame() lança IOException,


InterruptedException {
var endpoint =
new WordzEndpoint("localhost", 8080);

Os dois parâmetros passados para o construtor WordzEndpoint definem o host e a porta em que
o endpoint da web será executado.

2. Utilizando o IDE, geramos a classe:

pacote com.wordz.adapters.api;

classe pública WordzEndpoint {


public WordzEndpoint(String host, porta int) { }

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.

3. Crie um WebServer usando a biblioteca Molecule:

pacote com.wordz.adapters.api;

importar com.vtence.molecule.WebServer;

classe pública WordzEndpoint {


servidor WebServer final privado;

public WordzEndpoint(String host, porta int) {


servidor = WebServer.create(host, porta);
}
}
Machine Translated by Google

Iniciando um novo jogo 289

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.

Adicionando rotas ao servidor HTTP


Para ser útil, o endpoint HTTP deve responder aos comandos HTTP, interpretá-los e enviá-los como
comandos para nossa camada de domínio. Como decisões de design, decidimos o seguinte:

• 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

Para adicionar rotas ao servidor HTTP, faça o seguinte:

1. Teste a rota /start . Para trabalhar em pequenos passos, inicialmente retornaremos um código
de resposta HTTP NOT_IMPLEMENTED :

classe pública WordzEndpoint {


servidor WebServer final privado;

public WordzEndpoint (String host, int porta) {servidor = WebServer.create


(host, porta);

tente
{ server.route(new Routes() {{ post("/start")

.to(solicitação -> startGame(solicitação));


}}); }
catch (IOException ioe) { throw new
IllegaStateException (ioe);
}
}

resposta privada startGame(solicitação de solicitação) {


resposta de retorno
.of(HttpStatus.NOT_IMPLEMENTED) .done();
Machine Translated by Google

290 Conduzindo a camada da Web

}
}

2. Podemos executar o teste de integração WordzEndpointTest :

Figura 15.2 – Um status HTTP incorreto

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.

Conectando-se à camada de domínio


Nossa próxima tarefa é receber uma solicitação HTTP e traduzi-la em chamadas da camada de domínio. Isso envolve
analisar dados de solicitação JSON, usando a biblioteca Google Gson, em objetos Java e, em seguida, enviar esses
dados de resposta para a porta da classe Wordz :

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)

classe pública WordzEndpointTest {


@Zombar

privado Wordz mockWordz;

@Teste

void startGame() lança IOException,


InterruptedException {
ponto de extremidade var
Machine Translated by Google

Iniciando um novo jogo 291

= novo WordzEndpoint(mockWordz, "localhost",


8080);

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:

classe pública WordzEndpoint {


servidor WebServer final privado;
final privado Wordz wordz;

public WordzEndpoint(Wordz wordz,


String host, porta interna) {
isto.palavraz = palavraz;

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 privada startGame(solicitação de solicitação) {


tentar {
Jogador jogador
= novo Gson().fromJson(request.body(),
Jogador.class);

boolean isSuccessful = wordz.newGame(player); if (é bem-sucedido) {

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

292 Conduzindo a camada da Web

4. Agora podemos executar o teste, porém ele falha:

Figura 15.3 – Uma resposta HTTP incorreta

Falha porque o valor de retorno de wordz.newGame() era falso. O objeto simulado precisa ser
configurado para retornar verdadeiro.

5. Retorne o valor correto do stub mockWordz :

@Teste

void startGame() lança IOException,

InterruptedException {
ponto de extremidade var

= novo WordzEndpoint(mockWordz,
"localhost", 8080);

quando(mockWordz.newGame(eq(PLAYER)))
.entãoReturn(verdadeiro);

6. Em seguida, execute o teste:

Figura 15.4 – O teste passa


Machine Translated by Google

Iniciando um novo jogo 293

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.

Refatorando o código inicial do jogo


Como sempre, quando um teste é aprovado, consideramos o que – se houver alguma coisa – precisamos refatorar.

Valerá a pena refatorar o teste para simplificar a escrita de novos testes, agrupando o código comum em
um só lugar:

@ExtendWith(MockitoExtension.class) classe pública

WordzEndpointTest {
@Zombar

privado Wordz mockWordz;

ponto de extremidade privado do WordzEndpoint;

jogador final estático privado PLAYER


= novo Jogador("alan2112");

privado final HttpClient httpClient =


HttpClient.newHttpClient();

@BeforeEach

void setUp() { endpoint


= new WordzEndpoint(mockWordz,
"localhost", 8080);

@Teste

void startGame() lança IOException,


InterruptedException {
quando(mockWordz.newGame(eq(player)))
.entãoReturn(verdadeiro);

var req = requestBuilder("iniciar")


.POST(asJsonBody(PLAYER))
.construir();
Machine Translated by Google

294 Conduzindo a camada da Web

var res

= httpClient.send(req,
HttpResponse.BodyHandlers.discarding());

afirmarIsso(res)

.hasStatusCode(HttpStatus.NO_CONTENT.code);
}

private HttpRequest.Builder requestBuilder (Caminho da string) {

retornar HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/" + caminho));

privado HttpRequest.BodyPublisher asJsonBody (


Fonte do objeto) { return
HttpRequest.BodyPublishers
.ofString(new Gson().toJson(fonte));
}
}

Lidando com erros ao iniciar um jogo

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:

1. Escreva o teste para retornar um Conflito 409 se o jogo já estiver em andamento:

@Teste

void rejeitaRestart() lança Exception


{when(mockWordz.newGame(eq(player))) .thenReturn(false);

var req = requestBuilder("iniciar")


.POST(asJsonBody(jogador))
Machine Translated by Google

Iniciando um novo jogo 295

.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:

Figura 15.5 – Um teste com falha

3. Teste o código para informar que o jogo não pode ser reiniciado:

resposta privada startGame(solicitação de solicitação) {


tentar {
Jogador jogador
= novo Gson().fromJson(request.body(),
Jogador.class);

boolean isSuccessful = wordz.newGame(player); if (é bem-sucedido) {

retornar
Resposta .of(HttpStatus.NO_CONTENT) .done();

}
Machine Translated by Google

296 Conduzindo a camada da Web

resposta de retorno
.of(HttpStatus.CONFLITO)
.feito();

} catch (IOException e) { lançar new


RuntimeException (e);
}
}

4. Execute o teste novamente:

Figura 15. 6 – O teste passa

O teste é aprovado quando executado sozinho, agora que a implementação está em vigor. Vamos
executar todos os testes WordzEndpointTests para verificar nosso progresso.

5. Execute todos os WordzEndpointTests:

Figura 15.7 – Falha no teste devido à reinicialização do servidor

Inesperadamente, os testes falham quando executados um após o outro.

Corrigindo testes com falha inesperada


Quando executamos todos os testes, eles falham. Todos os testes anteriores foram executados corretamente quando executados
um de cada vez. Uma mudança recente claramente quebrou alguma coisa. Perdemos nosso isolamento de teste em algum
momento. Esta mensagem de erro indica que o servidor web está sendo iniciado duas vezes na mesma porta, o que não é possível.
Machine Translated by Google

Iniciando um novo jogo 297

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);

ponto final = novo WordzEndpoint(mockWordz, "localhost", 8080);

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)

@TestInstance(TestInstance.Lifecycle.PER_CLASS) classe pública


WordzEndpointTest {

Ambos os testes no WordzEndpointTest agora passam.

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:

resposta privada startGame(solicitação de solicitação) {


tentar {
Jogador jogador = extractPlayer(solicitação);

boolean isSuccessful = wordz.newGame(player);


Status do HTTPStatus
= foi bem sucedido?

HttpStatus.NO_CONTENT:
HttpStatus.CONFLITO;

resposta de retorno
.of(estado)
.feito(); }

catch (IOException e) { lançar new


RuntimeException (e);
Machine Translated by Google

298 Conduzindo a camada da Web

}
}

extractPlayer de player privado (solicitação de solicitação)

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

Jogando o jogo 299

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);

var adivinhaRequest = new GuessRequest(player, "-U---"); var body = new


Gson().toJson(guessRequest); var req = requestBuilder("adivinha")

.POST(ofString(corpo))
.construir();

3. A seguir definimos o registro:

pacote com.wordz.adapters.api;

importar com.wordz.domain.Player;

registro público GuessRequest(Player player, String palpite) { }

4. Em seguida, enviamos a solicitação via HTTP para nosso endpoint, aguardando a resposta:

@Teste

void parcialmenteCorrectGuess() lança Exception { var score = new Score("-


U---");
pontuação.assess("GUESS");
var resultado = new GuessResult(pontuação, falso, falso);
quando(mockWordz.assess(eq(player), eq("GUESS")))
.thenReturn(resultado);

var adivinhaRequest = new GuessRequest(player, "-U---"); var body = new


Gson().toJson(guessRequest);
var req = requestBuilder("adivinha")
.POST(ofString(corpo)) .build();
Machine Translated by Google

300 Conduzindo a camada da Web

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

void parcialmenteCorrectGuess() lança Exception { var score = new Score("-U--G");

pontuação.assess("GUESS");

var resultado = new GuessResult(pontuação, falso, falso);

quando(mockWordz.assess(eq(player), eq("GUESS")))
.thenReturn(resultado);

var palpiteRequest = new GuessRequest(jogador,


"-U--G");

var body = new Gson().toJson(guessRequest);

var req = requestBuilder("adivinha")

.POST(ofString(corpo)) .build();

var res

= httpClient.send(req,

HttpResponse.BodyHandlers.ofString());

resposta var

= novo Gson().fromJson(res.body(),

GuessHttpResponse.class);

// Chave das letras nas pontuações(): // C correto,

parte P correta, X incorreto Assertions.assertThat(response.scores())

.isEqualTo("PCXXX");

Asserções.assertThat(response.isGameOver())
Machine Translated by Google

Jogando o jogo 301

.é 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;

registro público GuessHttpResponse (Pontuações de string,


booleano isGameOver) {
}

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():

public WordzEndpoint(Wordz wordz, String host, porta int) {

isto.palavraz = palavraz;
servidor = WebServer.create(host, porta);

tente
{ server.route(new Routes() {{ post("/start")

.to(solicitação -> startGame(solicitação));


post("/adivinha")
.to(solicitação -> adivinhaWord(solicitação));
}}); }
catch (IOException e) { lançar new
IllegalStateException (e);
}
}

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

302 Conduzindo a camada da Web

8. Implementamos o método guessWord() com código para extrair os dados da solicitação do


Corpo da postagem :

resposta privada adivinhaWord( Solicitação de solicitação) {


tentar {
AdivinhaSolicitação gr =
extrairGuessRequest(solicitação);
retornar nulo;

} catch (IOException e) { lançar new


RuntimeException (e);
}

} private GuessRequest extractGuessRequest ( solicitação de solicitação) lança IOException


{
retornar novo Gson().fromJson(request.body(), GuessRequest.class);

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:

resposta privada adivinhaWord(Solicitação de solicitação) {


tentar {
GuessRequest gr =
extractGuessRequest(solicitação);
Resultado do GuessResult

= palavraz.avaliar(gr.player(),
gr.adivinha());
retornar nulo;
} catch (IOException e) { lançar new
RuntimeException (e);
}
}
Machine Translated by Google

Jogando o jogo 303

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:

resposta privada adivinhaWord(Solicitação de solicitação) {


tentar {
GuessRequest gr =
extractGuessRequest(solicitação); Resultado do
GuessResult = wordz.assess(gr.player(),
gr.adivinha());
retornar Response.ok()
.body(createGuessHttpResponse(resultado))
.feito();

} catch (IOException e) { lançar new


RuntimeException (e);
}
}

private String createGuessHttpResponse (resultado GuessResult) {

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;

classe pública GuessHttpResponseMapper { public


GuessHttpResponse de (resultado GuessResult) {
retornar nulo;

}
}
Machine Translated by Google

304 Conduzindo a camada da Web

12. Isso é suficiente para compilar e executar o teste WordzEndpointTest :

Figura 15.8 – O teste falha

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.

14. Depois de testarmos a implementação detalhada da classe GuessHttpResponseMapper,


podemos executar novamente o teste de integração:

Figura 15.9 – O teste do endpoint passa

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

Integrando o aplicativo 305

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;

importar com.wordz.adapters.api.WordzEndpoint; importar


com.wordz.adapters.db.GameRepositoryPostgres; importar
com.wordz.adapters.db.WordRepositoryPostgres; importar com.wordz.domain.Wordz;

classe pública WordzApplication { public static


void main(String[] args) {
var config = new WordzConfiguration(args); novo
WordzApplication().run(config);
}

private void run (configuração do WordzConfiguration) {


var gameRepositório
= novo GameRepositoryPostgres(config.getDataSource());

var palavraRepositório
= novo WordRepositoryPostgres(config.getDataSource());

var randomNumbers = new ProductionRandomNumbers();

var palavraz = new Wordz(gameRepository, wordRepository,


randomNumbers);

var api = new WordzEndpoint(wordz,


config.getEndpointHost(),
config.getEndpointPort());

waitUntilTerminated();
}
Machine Translated by Google

306 Conduzindo a camada da Web

private void waitUntilTerminated() {


tentar {
enquanto (verdadeiro) {
Thread.sleep(10000);
}
} catch (InterruptedException e) {
retornar;
}
}
}

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

Usando o aplicativo 307

Figura 15.10 – Tela inicial do Postman

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:

1. Crie uma solicitação POST para iniciar o jogo:

Figura 15.11 – Iniciar um novo jogo


Machine Translated by Google

308 Conduzindo a camada da Web

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:

wordzdb=# selecione * do jogo;


nome_jogador | palavra | número_de_tentativa | is_game_over
-------------+-------+-----+----------- ---
usuário de teste | LEVANTA-SE | 0|f
(1 linha)

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:

Figura 15.12 – Pontuação retornada


Machine Translated by Google

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.

A resposta também mostra "isGameOver":false. Ainda não terminamos o jogo.

3. Faremos mais um palpite, trapaceando um pouco. Vamos enviar uma solicitação POST com um palpite
de "ARISE":

Figura 15.13 – Um palpite bem-sucedido

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.

Jogamos com sucesso um jogo de Wordz usando nosso microsserviço.

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

310 Conduzindo a camada da Web

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

• Java OOP feito corretamente, Alan Mellor, ISBN 9781527284449

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

Leitura adicional 311

• http://molecule.vtence.com/

Uma estrutura HTTP leve para Java

• https://undertow.io/

Um servidor HTTP para Java que funciona bem com a estrutura Molecule

• https://github.com/google/gson

Biblioteca do Google para conversão entre objetos Java e o formato JSON

• https://aws.amazon.com/what-is/restful-api/

Guia da Amazon para APIs REST

• 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

ferramentas de teste de aceitação 186, 187 propósito 5

adaptadores 156, 180 escrever 3, 4, 5

substituindo, com testes duplos 166, 167 código incorreto, reconhecer 6

desenvolvimento ágil 50 coesão 10

combinando, com TDD 52, 53 acoplamento 10

explorando 50, 51 construções propensas a erros 9, 10

histórias de usuários, lendo 51, 52 alto acoplamento, entre classes 12 baixa


Código padrão americano para informações coesão, na classe 10, 11 namespace

Intercâmbio (ASCII) 95 7-9 problemas técnicos

Programação de aplicativos 6, 7 componente de caixa


Interface (API) 49, 77 preta 77 implantação azul-verde

correspondentes de argumentos 142-144 193 testes quebrados

Análise automatizada de código Arrange-

Act-Assert (AAA) 56 escrita, diz respeito a 35


benefícios 204 bibliotecas de
limitações 204 arquivos build.gradle , adicionando 284

testes automatizados 202, 203, 221 resultados de negócios

diminuindo 14, 15, 16


Machine Translated by Google

314 Índice

implantação contínua (CD) 190 definindo


C 187 integração
Técnica de teste de caracterização 39 contínua (CI) definindo 187 meta
Chicago TDD 223 188 necessidade
Chrysler abrangente de 187, 188
Projeto de compensação 200

Pipelines CI/CD 187, 190 estágios abordagem de integração contínua/entrega

190, 191 contínua (CI/CD) 209 acoplamento 10


Elementos manuais de

fluxos de trabalho de CI/CD 209, 210 Pepino


Código classicista TDD URL 186
223 complexidade ciclomática (CYC) 21, 220

documentando 28, 29 test-


drive, para jogar o jogo 298-304 código e
correção estilo 32 métrica
D
de cobertura de código projeto de

evitando, como alvo 216, 217 banco de dados

ferramenta de cobertura de 173 palavras, buscando 194-197

código 216 legibilidade adaptadores de banco


do código 83 revisão de dados testando

de código 203 abordagens 181, 182 teste de integração de


204, 205 banco de dados

definição de criando 268, 269 código de


cheiro de código 82 produção 272-276
link de referência componentes de porta
82 coesão 10 colaboradores de banco de dados 162 piloto de banco de dados 195

tratamento de erros, desafios de teste 121 Teste de

testes, desafios 120, 121 banco de dados DBRider , criando com


comportamento irrepetível, desafios caminho de código
de teste 120, 121 morto 269-271 21 injeção de
Engenharia de software auxiliada por computador dependência 125 inversão de
(CASE) ferramentas 215 dependência (DI) 104, 125, 153, 154

Objeto de configuração 66 Princípio de Inversão de Dependência (DIP) 130


teste de contrato orientado ao consumidor 183, 184 aplicando-se, às formas do código 106-109,
entrega contínua (CD) 190 benefícios detalhes irrelevantes, ocultando 104-106
189 definição

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

ambiente de desenvolvimento problemas ambientais 151 chamadas


preparando 46 de sistema operacional 152
Método de tempo do sistema
diagnóstico DevOps 208 152 serviços de terceiros 152, 153
241 domínio dados incertos 152

20 camada de Programação Extrema (XP) 28


domínio conectando-se ao
modelo de domínio 290-293
F
chamadas, abstraindo para serviços web 163,
código 164, escrevendo falha na
164 banco de dados, abstraindo 162, escrita do teste
163 decidindo 164, 285-287 teste falso negativo
165 frameworks, usando 165 180 rápido, isolado, repetível, autoverificável e
bibliotecas, usando oportuno (FIRST) 60, 61, 236

abordagem de programação 165, decidindo 165 sinalizadores de


solicitações e respostas web, recursos 210
abstraindo 161, 162 testes instáveis 180
método estrangeiro 241 programação funcional
(FP) 20, 158 defeitos futuros
E
protegendo contra 27, 28 link
testes ponta a ponta 177, 184-186 de
vantagens e desvantagens 184 referência difuso 208

programação em conjunto 205


teste de condição

de erro, em Wordz 145-147


G
código de tratamento getters 241
de erros com testes métodos getXxx() 241
144, 145 Lei de Goodhart 216

exceções afirmando Link de referência 6 do

63, 64 testes exploratórios 202 guia de estilo do Google

Google Tricorder 204 fase


verde 77, 78
Machine Translated by Google

316 Índice

Link para
H
download do IntelliJ

testes manuais do caminho feliz 5 IDE 46 instalando 46


Alerta de falso ataque com mísseis no Havaí Uso eficaz de interfaces 115 do Princípio de
ligação de referência 152 Segregação de Interface

arquitetura hexagonal 153-155 (ISP) , revisando nos códigos de formas 115-117


adaptadores, testando Inversão de Controle (IoC) 125

227 lógica de domínio, testando com 226, Visão

227 regra de ouro URL 207

159 formato hexagonal, necessidade


de 160 limites de teste, definindo com 226 J.
histórias de usuário, testando 228,
229 arquitetura hexagonal, componentes
adaptadores, conectando-se às portas 157, Bibliotecas e projetos Java , configurando 46-48
158 sistemas externos conectam-se aos Brincadeira

adaptadores 156 URL 206

com visão geral de 155 portas, conectando-se ao


modelo de domínio 158, K
159 alta coesão 116 alto acoplamento 12
Servidor HTTP mantenha as coisas simples, estúpido (KISS) 71

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

181 vantagens 180 lógicas teste


testes de contrato orientados ao consumidor 183, manual, limites 26 prevenção
184 adaptadores de banco de dados, 25 teste,
testes 181, 182 para automatização para resolver problemas 26, 27
banco de dados baixa coesão 10

194 limitações 180 web serviços, testes 182


Machine Translated by Google

Índice 317

Princípio Aberto-Fechado (OCP)


M acrescentando ao novo tipo de código de forma 114,
teste exploratório manual 201-203 limites de 115 design extensível 112, 113
teste manual 26 bibliotecas HTTP de código aberto 284
nome do abordagem TDD de fora para dentro 224,
método 19 225 vantagens e desvantagens 225
Mockito 244 Os 10 principais aplicativos da Web da OWASP

distinção entre argumento matcher Link de referência

142-144, confusão entre stubs e de riscos de segurança 208


mocks 142

URL 133

usado, para escrever simulação 141,


P
142 usado, para escrever stub Pacto

133-141 método verify() URL 184

142 método when() 142 programação em pares 38, 205


trabalhar com 133 teste de penetração (pentesting) 208
objetos simulados Portas e Adaptadores 155
127 uso excessivo, evitando Banco de dados Postgres

130 uso, cenários 132 instalando 267.268


usando, para verificar interações 127-130 stubs de resultados
código pré-preparados ,
simulado , evitando para classe concreta usando 126 programação processual
escrito fora da equipe 130, 131 20 métodos públicos
injeção de dependência 131, 132 encapsulamento, preservação 64, 65
testes, evitação de 132 teste 64
objetos de valor, evitando 131 modelo pull 127
solicitação pull 204
modelo push 127
N
Net Promoter Score®™ (NPS) 14
P
Engenheiro de Garantia de Qualidade (QA)
Ó
15 abstração
design orientado a objetos (OO) 97 de código de qualidade

programação orientada a objetos 19, 20 complexidade acidental, evitando 20-22


(POO) 20, 95, 158 projeto 17, 18
fiação de objeto 125 objetivo
18 ocultação de informações 19,
20 nomeação 18, 19
Machine Translated by Google

318 Índice

Falsificação, adulteração, repúdio,


R Divulgação de informações, negação

Registros RAID de serviço e elevação de

215 adaptadores de números Privilégio (STRIDE)

aleatórios link de referência 208

projetando 173 ciclo de refatoração vermelho, Primavera

verde (RGR) 75-79 fase URL 125

de refatoração 78, 79 ambiente de preparação 191


testes de regressão stub

201 repositório 162 interfaces usando, para resultados pré-definidos 126


de repositório 168 projetando 169-173 gravação, com Mockito 133-140
Tenha certeza cenários de
URL 186 uso de objetos stub 127
Link de stubs e simulações
referência RestEasy distinção, confundindo entre 142 tipo de
186 biblioteca rider-junit5 269 ativação 105

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

DIP, aplicando 106-109 166 versão de

Uso do ISP, revisando 115-117 produção do código, criando 124,


Uso de LSP, revisando 111 125 finalidade 122-124

OCP, adicionando ao tipo de 114, 115 substituição, para


abordagem shift-left 222 sistemas externos 166 usados, para substituição
Blocos do Princípio de Responsabilidade de adaptadores 166, 167 usando, cenários
Única (SRP) , edifício 97-104 130 orientados a testes
Princípios SÓLIDOS 37, 97 aplicação integrando 304-306
Sonarqube
URL 203 Desenvolvimento Orientado a Testes (TDD) 3, 31, 95
pico 40, 41 desenvolvimento ágil, combinado com 52, 53 testes
de regressão automatizados 201
benefícios, de desaceleração 32, 33
Machine Translated by Google

Índice 319

design emergente 217 PRIMEIROS princípios, aplicando 60, 61

expectativas, gerenciamento 37 faltando 24 em

guia 96, 97 execução, na produção 192 afirmação


limites 200 única, usando por teste 61 escrita, para

mitos 31-39 aplicação Wordz 79-85 testes, escrita após o

objeções a não detectar todos os bugs, código, versus


superando 35 objeções a testes escritos antes do código 218

testes que retardam os usuários,


superando 33 expectativas benefícios, analisando antes do

infladas por problemas 36 teste, escrevendo código de produção 23, 24

antes do código de produção 40 abstrações com vazamento, evitando 25


ambientes de teste 181, 187, 191 particionamento de tráfego 193, 194

vantagens e desvantagens 191 teste triangulação 73, 86


primeiro , combinações de duas letras

como ferramenta de design 214 design, avançando com 85-92

Estágio do ato

214 somando 213 você

Organizar o estágio 214

Afirmar métricas de Diagrama UML

cobertura de código do estágio 215 215, para formas código 98

216 formam especificações executáveis 215 Linguagem de Modelagem Unificada (UML) 97

usadas, para tomar decisões de design 214 testes unitários

escrever, em entrega contínua em unidades maiores


situações 218 167 histórias de usuários 167, 168

cronograma de teste 222 testes unitários 177-179

benefícios vantagens 178


de teste Arrange-Act-Assert (AAA) 56 modelo de
posterior 219 limitação domínio 179 limitações

219-221 pirâmide de teste 66, 178 resultados,


176-178 testes 65 trabalhando para trás 58 escopo, decidindo

agir passo 66 61 estrutura, definindo

organizar passo 65 56-58 fluxo de trabalho,

afirmar passo 66 aumentando 59 código não

automatizar, para resolver problemas 26, 27 testável

cobertura de código 67 causa 37, 38

erros comuns, capturando 62, 63 definindo testes de aceitação do usuário 177, 184-186

59 código de

tratamento de erros com 144, 145


Machine Translated by Google

320 Índice

avaliação da Código do jogo

experiência do Wordz , test-drive 298-304,


usuário 207 interface do usuário codificação da

(UI) 27, 33 testes camada de domínio 235, conexão com 290-293,


205, 206 histórias de finalização de

usuários 50, 51 254 erros, tratamento de teste


leitura 51, 52 seções 52 com falha 294-296, correção do

teste com falha 296-298, gravação de 285-287

Servidor HTTP, criando 288


V Rotas do servidor HTTP, adição de 289.290

objeto de valor 64, 131 bibliotecas, adição ao progresso do


nome da variável 19 projeto 284, rastreamento da

método verify() 142 interface de pontuação 238-254, design do

código inicial do jogo 250-252, refatoração

293 início 284


C test-drive 236, lógica de

desenvolvimento em cascata 50 seleção de 237 palavras, triangulação 244-249


serviços web Jogo Wordz, detecção de final de jogo ,

testando 182 suposição correta, resposta 255, design 256,


quando() método 142 revisão de 260-263 número

Implementação do adaptador máximo de suposições, triangulação


WordRepository 276, 277 de 256, resposta 257,

banco de dados, acessando 277-279 triangulação para adivinhar após o jogo

GameRepository, implementando 279 257-259


Palavraz 168 Aplicativo de serviço web Wordz

banco de dados, projetando usando, com Postman 306-309 testes

condição de erro 173, testando adaptadores errados

de números aleatórios 145-147, projetando interface escrevendo 67

de repositório 173, projetando 168-173 escrevendo

67-73

Aplicação Wordz 48 regras,


S
descrevendo 49 testes, você não vai precisar disso (YAGNI) 71, 250

escrevendo 79-85

palavras, buscando no banco de dados 194-197


Machine Translated by Google

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.

Por que assinar?


• Gaste menos tempo aprendendo e mais tempo codificando com e-books e vídeos práticos de mais de 4.000
profissionais do setor

• Melhore seu aprendizado com Planos de Habilidades criados especialmente para você

• Receba um e-book ou vídeo grátis todo mês

• Totalmente pesquisável para fácil acesso a informações vitais

• Copiar e colar, imprimir e marcar conteúdo

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

Outros livros que você pode gostar

Se você gostou deste livro, pode estar interessado nestes outros livros de Packt:

Desenvolvimento Pragmático Orientado a Testes em C# e .NET

Adam Tibi

ISBN: 978-1-80323-019-1

• Escrever testes unitários com xUnit e dominar a injeção de dependência • Implementar

testes duplos e zombar com Nsubstitute

• Usando o estilo TDD para testes unitários em conjunto com DDD e melhores práticas

• Combinando TDD com API ASP.NET, Entity Framework e bancos de dados •

Passando para o próximo nível explorando a integração contínua com GitHub

• Introdução a cenários de simulação avançados

• Defendendo sua equipe e empresa pela introdução de TDD e testes unitários


Machine Translated by Google

Outros livros que você pode gostar 323

Desenvolvimento orientado a testes com C++

Abdul Wahid Curtidor

ISBN: 978-1-80324-200-2

• Compreender como desenvolver software usando TDD •

Manter o código do sistema o mais livre de erros possível • Refatorar

e redesenhar o código com confiança

• Comunique os requisitos e comportamentos do código com sua equipe

• Compreender as diferenças entre testes unitários e testes de integração • Usar

TDD para criar uma estrutura de teste mínima viável


Machine Translated by Google

324

Packt está procurando autores como você

Se você estiver interessado em se tornar um autor da Packt, visiteauthors.packtpub.com e inscreva-se hoje.


Trabalhamos com milhares de desenvolvedores e profissionais de tecnologia, assim como você, para ajudá-
los a compartilhar suas ideias com a comunidade global de tecnologia. Você pode fazer uma inscrição geral,
candidatar-se a um tópico específico para o qual estamos recrutando um autor ou enviar sua própria ideia.

Compartilhe seus pensamentos

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

Baixe uma cópia gratuita em PDF deste livro


Obrigado por adquirir este livro!

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

Siga estas etapas simples para obter os benefícios:

1. Digitalize o código QR ou visite o link abaixo

https://packt.link/free-ebook/978-1-80323-623-0

2. Envie seu comprovante de compra

3. É isso! Enviaremos seu PDF grátis e outros benefícios diretamente para seu e-mail

Você também pode gostar