TDD - Test Driven Development
TDD, ou Desenvolvimento Orientado a Testes (Test-Driven Development), é uma prática de desenvolvimento de software que enfatiza a criação de testes automatizados antes da implementação do código funcional. O TDD é baseado na ideia de que os testes devem ser uma parte fundamental do processo de desenvolvimento, e não apenas uma verificação final.
Princípios do TDD
-
Escreva um Teste: Antes de escrever qualquer código, você deve escrever um teste que defina uma função ou melhoria desejada. Este teste deve falhar inicialmente, pois a funcionalidade ainda não foi implementada.
-
Implemente a Funcionalidade: Após escrever o teste, você deve implementar a mínima quantidade de código necessária para fazê-lo passar.
-
Refatore: Uma vez que o teste passa, você pode refatorar o código para melhorar sua qualidade, mantendo os testes verdes (passando). Refatoração é uma parte crítica do TDD, pois permite que você melhore o design do código sem alterar seu comportamento.
Benefícios do TDD
- Feedback Rápido: Permite detectar falhas rapidamente, já que os testes são executados frequentemente.
- Código Mais Limpo e Manutenível: Como o código é frequentemente refatorado, ele tende a ser mais limpo e mais fácil de manter.
- Documentação Viva: Os testes servem como uma forma de documentação do comportamento do sistema.
Ciclo Red-Green-Blue
O ciclo Red-Green-Blue é uma abordagem visual e prática para implementar o TDD. Cada cor representa uma fase no processo de desenvolvimento:
- Red (Vermelho):
-
Escreva um teste que valide uma nova funcionalidade. No início, o teste deve falhar, indicando que a funcionalidade ainda não foi implementada. Essa fase é importante porque confirma que o teste está, de fato, verificando algo significativo.
-
Green (Verde):
-
Implemente a funcionalidade mínima necessária para que o teste passe. O objetivo aqui é garantir que a implementação atenda ao teste. Assim que o teste passa, você pode dizer que a funcionalidade está "verde".
-
Blue (Azul):
- Refatore o código. O objetivo é melhorar a estrutura e a legibilidade do código, garantindo que todos os testes ainda passem. O termo "azul" pode não ser comum; muitas vezes, a etapa de refatoração é simplesmente chamada de "refatoração", mas a ideia é que agora você está em um estado "estável" e "limpo".
TDD e o ciclo Red-Green-Blue são práticas poderosas que podem melhorar significativamente a qualidade do código e a eficiência do desenvolvimento. Ao escrever testes antes de implementar funcionalidades, os desenvolvedores podem garantir que o código atenda aos requisitos desde o início e que permaneça robusto e flexível à medida que evolui. Essa abordagem não apenas melhora a qualidade do software, mas também ajuda a construir uma mentalidade de qualidade e responsabilidade entre os desenvolvedores.
Exemplo de uso do TDD
Abordaremos a implementação de um sistema de gerenciamento de personagens em um jogo, onde os personagens podem ter atributos como força de ataque e defesa, além de equipar armaduras, armas e capacetes. O sistema será desenvolvido inicialmente de forma funcional e, em seguida, refatorado para uma abordagem orientada a objetos (OOP).
1. Criando o Teste
Usaremos o framework pytest para criar testes unitários que verificarão se o sistema de personagens funciona conforme o esperado. Os testes incluirão:
- Verificação da criação de um personagem.
- Adição de armaduras, armas e capacetes.
- Cálculo correto da força de ataque e defesa.
tests.py
import pytest
from character import *
def test_character_creation():
create_character(1, 'Guerreiro Valente')
stats = get_stats(1)
assert stats['nome'] == 'Guerreiro Valente'
assert stats['ataque'] == 10
assert stats['defesa'] == 5
def test_add_armor():
create_character(2, 'Cavaleiro Forte')
add_armor(2, 1) # Adiciona Cota de Malha
stats = get_stats(2)
assert stats['defesa'] == 8 # 5 base + 3 da armadura
def test_add_weapon():
create_character(3, 'Lutador Rápido')
add_weapon(3, 2) # Adiciona Machado
stats = get_stats(3)
assert stats['ataque'] == 15 # 10 base + 5 da arma
def test_add_helmet():
create_character(4, 'Guerreiro Blindado')
add_helmet(4, 2) # Adiciona Capacete de Aço
stats = get_stats(4)
assert stats['defesa'] == 8 # 5 base + 3 do capacete
Rodar Para Falhar (RED)
É uma boa prática rodar os testes antes de implementar qualquer funcionalidade. Isso garante que, se houver alguma falha nos testes, saberemos que precisa ser corrigida. Portanto, ao executar o comando pytest, devemos garantir que todos os testes falhem inicialmente, indicando que a implementação não está completa.
2. Implementação Funcional
Abaixo está a implementação funcional do sistema de gerenciamento de personagens:
character.py
# Implementação Funcional
personagens = {}
def create_character(id_personagem, nome):
"""Cria um novo personagem com atributos iniciais."""
personagens[id_personagem] = {
'nome': nome,
'ataque': 10, # Força de ataque base
'defesa': 5, # Força de defesa base
'armaduras': [],
'armas': [],
'capacetes': []
}
def add_armor(id_personagem, id_armor):
armor_stats = {
1: {'nome': 'Cota de Malha', 'bonus_defesa': 3},
2: {'nome': 'Armadura de Couro', 'bonus_defesa': 2},
3: {'nome': 'Armadura Pesada', 'bonus_defesa': 5}
}
if id_personagem in personagens and id_armor in armor_stats:
armor = armor_stats[id_armor]
personagens[id_personagem]['defesa'] += armor['bonus_defesa']
personagens[id_personagem]['armaduras'].append(armor['nome'])
def add_weapon(id_personagem, id_weapon):
weapon_stats = {
1: {'nome': 'Espada Curta', 'bonus_ataque': 4},
2: {'nome': 'Machado', 'bonus_ataque': 5},
3: {'nome': 'Lança', 'bonus_ataque': 3}
}
if id_personagem in personagens and id_weapon in weapon_stats:
weapon = weapon_stats[id_weapon]
personagens[id_personagem]['ataque'] += weapon['bonus_ataque']
personagens[id_personagem]['armas'].append(weapon['nome'])
def get_stats(id_personagem):
"""Retorna a força de ataque e defesa do personagem."""
if id_personagem in personagens:
return personagens[id_personagem]
return None
Rodar o teste para passar (GREEN)
Rodar o teste a essa altura vai ocasionar que o mesmo passe com sucesso, estamos na fase em que já temos uma versão funcional do código
3. Refatoração para Objetos
Após a implementação funcional, o próximo passo é refatorar o código para uma abordagem orientada a objetos. A refatoração envolverá criar uma classe Personagem que encapsula os atributos e métodos necessários para manipular os personagens.
Implementação Orientada a Objetos
@dataclass
class Armadura:
nome: str
bonus_defesa: str
@dataclass
class Arma:
nome: str
bonus_ataque: str
@dataclass
class Personagem:
id_personagem: int
nome: str
forca_ataque: int = 10
forca_defesa: int = 5
armaduras:list[Armadura] = []
armas:list[Arma] = []
def add_armadura(self, armadura: Armadura):
self.armaduras.append(armadura)
def add_arma(self, arma: Arma):
self.armas.append(arma)
@property
def ataque(self):
return sum([self.forca_ataque, *[a.bonus_ataque for a in self.armas]])
@property
def defesa(self):
return sum([self.forca_ataque, *[a.bonus_defesa for a in self.armaduras]])
def get_stats(self):
return {
'nome': self.nome,
'ataque': self.ataque,
'defesa': self.defesa
}
personagens = {}
aramaduras = {
1: Armadura('Cota de Malha', 3),
2: Armadura('Armadura de Couro', 2),
3: Armadura('Armadura Pesada', 5),
}
armas = {
1: Arma('Espada Curta', 4),
2: Arma('Machado', 5),
3: Arma('Lança', 3),
}
def create_character(id_personagem, nome):
personagems[id_personagem] = Personagem(1, nome)
def add_armor(id_personagem, id_armor):
if id_personagem in personagens and id_armor in armor_stats:
armor = armor_stats[id_armor]
personagems[id_personagem].add_armor(armor)
def add_weapon(id_personagem, id_weapon):
if id_personagem in personagens and id_weapon in weapon_stats:
weapon = weapon_stats[id_weapon]
personagens[id_personagem].add_weapon(weapon)
def get_stats(id_personagem):
"""Retorna a força de ataque e defesa do personagem."""
if id_personagem in personagens:
return personagens[id_personagem].get_stats()
return None
Refatorando sem mudar os testes (BLUE)
A refatoração para OOP não requer mudanças nos testes, pois a assinatura dos métodos e a lógica de manipulação de personagens permanecem as mesmas. A estrutura dos testes já criada para a implementação funcional funcionará com a nova implementação orientada a objetos, garantindo que o comportamento do sistema permaneça consistente.
Revisão dos Testes Unitários
Análise do Código Backend
A análise a seguir se concentra em testes do backend, com ênfase em problemas comuns que podem ser encontrados.
Problemas Comuns:
- Foco em Unidades Isoladas: Testes que se concentram em métodos específicos de classes, em vez de verificar fluxos de comportamento completos.
- Uso Excessivo de Mocks: Utilização de mocks para dependências internas que poderiam ser instanciadas diretamente.
- Acoplamento à Implementação: Testes que dependem de detalhes de implementação, como a ordem de chamadas de métodos internos.
Exemplo Problemático (Hipotético):
// Código Problemático
@Test
void testAutenticarUsuario() {
UsuarioRepository usuarioRepository = mock(UsuarioRepository.class);
AutenticacaoService autenticacaoService = new AutenticacaoService(usuarioRepository);
when(usuarioRepository.findByEmail(anyString())).thenReturn(Optional.of(new Usuario()));
autenticacaoService.autenticarUsuario("email@example.com", "senha");
verify(usuarioRepository).findByEmail("email@example.com");
}
Problemas Identificados:
- Uso Excessivo de Mocks: O repositório poderia ser substituído por um banco de dados em memória, proporcionando um teste mais próximo da realidade.
- Foco no Método: O teste deveria se concentrar no resultado da autenticação, em vez de apenas verificar se um método foi chamado.
Código Corrigido (Exemplo):
// Código Corrigido (Exemplo)
@Test
void testAutenticarUsuarioComSucesso() {
// Configuração: Criar um usuário no banco de dados em memória
Usuario usuario = new Usuario();
usuario.setEmail("email@example.com");
usuario.setSenha(PasswordEncryption.encrypt("senha"));
usuarioRepository.save(usuario);
// Ação: Tentar autenticar o usuário
UsuarioResponseDTO resultado = autenticacaoService.autenticarUsuario("email@example.com", "senha");
// Verificações: Confirmar o resultado da autenticação
assertNotNull(resultado);
assertEquals("email@example.com", resultado.getEmail());
}
Melhorias Propostas:
- Uso de um banco de dados em memória para simular a interação real com o repositório.
- Foco no resultado da autenticação, que é o comportamento desejado.
Análise do Código Frontend
A análise a seguir se concentra em testes do frontend, destacando problemas comuns.
Problemas Comuns:
- Excesso de Mocks: Mocks de componentes, hooks e funções que poderiam ser testados diretamente.
- Testes Superficiais: Verificação apenas da renderização de componentes, sem considerar interações e resultados.
- Acoplamento a Detalhes de Implementação: Testes que dependem da estrutura interna dos componentes.
Exemplo Problemático:
// Código Problemático
vi.mock('next/navigation', () => ({
useRouter: vi.fn(),
}));
describe('RootLayout', () => {
beforeEach(() => {
const push = vi.fn();
(useRouter as jest.Mock).mockReturnValue({ push });
});
it('renders the header and child components', () => {
const { getByText } = render(<PrivateLayout>Child Component</PrivateLayout>);
expect(getByText(/Journey/i)).to.exist;
expect(getByText(/Child Component/i)).to.exist;
});
});
Problemas Identificados:
- Uso Desnecessário de Mocks: O mock do
useRouteré desnecessário a menos que o teste dependa de rotas específicas. - Foco na Renderização: O teste se limita a verificar a presença de texto, sem avaliar se o componente funciona corretamente.
Código Corrigido (Exemplo):
// Código Corrigido (Exemplo)
describe('RootLayout', () => {
it('renders the header and child components', () => {
const { getByText } = render(<PrivateLayout>Child Component</PrivateLayout>);
expect(getByText(/Journey/i)).to.exist;
expect(getByText(/Child Component/i)).to.exist;
// Aqui você pode adicionar interações e verificações adicionais
});
});
Melhorias Propostas:
- Remover o mock do
useRouter, a menos que seja estritamente necessário. - Focar em testar o comportamento do componente, como interações e exibição de dados.
Outras Sugestões
-
Utilizar Bibliotecas de Teste Mais Expressivas: A biblioteca
react-testing-libraryincentiva testes que se concentram no comportamento do usuário, em vez de detalhes de implementação. -
Criar Cenários de Teste Abrangentes: Testar diferentes estados e interações do componente, garantindo que o sistema se comporte adequadamente em várias situações.
-
Evitar Asserções Excessivas: Cada teste deve ter um objetivo claro e verificar apenas o resultado esperado, evitando sobrecarga de verificações desnecessárias.
Trabalhando com a IA e testes TDD
A IA não aplica os princípios do TDD de bate pronto. É necessário descrever exatamente o que o TDD prega antes de pedir para ela gerar um teste, segue abaixo um prompt efetivo para que ela gere esses testes ou melhore os testes existetes em sua aplicação
O TDD prega que os testes devem ser de integração e baseados em comportamentos, desafiando o metodo tradicional de testes unitários. Isso requer que não sejam usados mocks para tudo, apenas de conexões externas cujas não possam ser usadas diretamente. O TDD também prega que os testes devem saber o menos possível sobre a implementação, no máximo deve conhecer a interface pública afim de gerar desacoplamento entre o teste e a implementação
Tendo isso em mente, melhore os testes existentes em minha aplicação
