Mantendo os Testes Valiosos: Testes Sociáveis no Coração do Software!
As responsabilidades de uma classe determinam se devem ser usados testes de unidade solitários ou sociáveis.
Você já se perguntou como garantir que o coração do seu software bata no ritmo certo? No mundo ágil e dinâmico do desenvolvimento de software, especialmente em arquiteturas de microserviços, enfrentamos um dilema crucial: escolher entre testes solitários e sociáveis. Mas, o que realmente diferencia um do outro? E mais importante, como saber qual abordagem é a melhor para o seu projeto? Será que podemos adotar os dois em momentos diferentes?
Neste artigo, vamos falar bastante sobre testes sociáveis. Vamos explorar suas origens, entender por que são considerados mais confiáveis e como se encaixam perfeitamente ao testar o domínio do seu software. Não se trata apenas de escolher uma técnica de teste; é sobre garantir a robustez e a confiabilidade do seu sistema em um ambiente de microserviços também.
Mas não se engane, cada moeda tem dois lados. Vamos discutir também os pontos de atenção e as considerações essenciais ao optar por testes sociáveis. Por fim, mas não menos importante, abordaremos como e quando fazer a escolha certa entre um teste sociável e um solitário, garantindo que você esteja equipado com o conhecimento necessário para tomar decisões informadas.
Se gostar do conteúdo, por favor, compartilhe e deixe seu like no post! Isso me ajuda e incentiva a continuar a trazer conteúdos em forma de texto também!😄
A Origem e o Conceito
Você já se perguntou como os engenheiros testam um carro novo antes de lançá-lo no mercado? Eles não se limitam a testar apenas o motor isoladamente ou apenas os freios em um ambiente controlado. Em vez disso, eles levam o carro para a estrada, testando todas as suas partes em conjunto, em condições reais de uso. Essa abordagem é muito semelhante ao conceito de testes sociáveis no desenvolvimento de software.
A origem do termo "testes sociáveis" é frequentemente atribuída a Martin Fowler, um renomado autor e especialista em desenvolvimento de software. Fowler discute o conceito de testes sociáveis em seu artigo intitulado "Testes de Unidade", publicado em seu website pessoal.
Martin Fowler, em seu artigo, distingue dois tipos principais de testes de unidade: os testes solitários e os testes sociáveis. Ele descreve os testes solitários como aqueles que testam uma unidade de código em isolamento, geralmente com o uso de mocks para simular as interações com outras partes do sistema. Por outro lado, os testes sociáveis são aqueles que permitem que a unidade de código sob teste interaja com implementações reais de suas dependências. A imagem abaixo retirada do artigo de Fowler representa muito bem as diferenças:
Pense nos testes sociáveis como uma festa onde todos os convidados (componentes do software) interagem uns com os outros. Em vez de conversar com cada convidado individualmente em um ambiente isolado, você observa como eles se comportam e interagem em um ambiente social. Essa interação revela muito mais sobre a dinâmica do grupo e as relações entre os convidados do que conversas individuais isoladas.
Entendendo mais!
Os testes sociáveis surgiram como uma resposta às limitações dos testes solitários. Enquanto os testes solitários focam em uma única unidade ou componente do software, isolando-o de suas dependências, os testes sociáveis levam em conta a interação dessa unidade com outras partes do sistema. É como comparar o teste de um motor de carro em um laboratório com o teste do carro inteiro em uma pista de corrida.
Para escrever um teste sociável precisamos ter em mente algumas coisas:
Testar Interações Reais: Em vez de usar mocks ou stubs, um teste sociável deve interagir com as dependências reais da unidade de código que está sendo testada. Isso significa que as classes, serviços ou componentes com os quais a unidade interage devem ser as implementações reais.
Foco no Comportamento do Sistema: O objetivo é verificar o comportamento do sistema como um todo, e não apenas a funcionalidade isolada de uma unidade. Isso inclui testar a integração e a comunicação entre diferentes unidades ou componentes.
Dados Realistas: Utilize dados de teste que representem cenários reais de uso. Isso ajuda a garantir que o teste reflita com precisão como o sistema se comportará em produção.
Ambiente de Teste Consistente: O ambiente em que os testes são executados deve ser o mais próximo possível do ambiente de produção.
Isolamento de Testes: Apesar de serem 'sociáveis', cada teste deve ser independente dos outros. Isso significa que a execução de um teste não deve afetar o estado ou o resultado de outro.
Automatização: Os testes sociáveis devem ser automatizados para facilitar a execução frequente e consistente, especialmente em ambientes de integração e entrega contínua.
Mas também existem limites, por isso vou listar três coisas que um teste sociável jamais deve fazer:
Não Deve Ignorar o Isolamento de Estado entre Testes:
Cada teste sociável deve ser independente dos outros. Isso significa que um teste não deve depender do estado deixado por outro teste anterior. Ignorar o isolamento de estado pode levar a resultados de testes inconsistentes e difíceis de replicar, onde um teste pode passar ou falhar dependendo da ordem em que é executado.
Não Deve Substituir Dependências Reais por Mocks ou Stubs:
A essência dos testes sociáveis é testar a interação entre diferentes partes do sistema em um ambiente o mais próximo possível do real. Substituir dependências reais por mocks ou stubs vai contra esse princípio. Embora em alguns casos possa ser necessário simular dependências externas (como serviços de terceiros), o uso excessivo de mocks pode transformar um teste sociável em um teste solitário, perdendo a vantagem de testar as interações reais.
Não Deve Comprometer a Performance do Sistema de Testes:
Embora os testes sociáveis sejam mais abrangentes, eles não devem ser tão pesados ou demorados a ponto de comprometer a eficiência do processo de desenvolvimento. Testes que são excessivamente lentos ou que consomem muitos recursos podem se tornar um obstáculo, especialmente em ambientes de integração e entrega contínua. É importante encontrar um equilíbrio entre a abrangência do teste e a eficiência operacional.
Manter essas considerações em mente é crucial para realizar testes sociáveis eficazes, que ofereçam insights valiosos sobre o funcionamento integrado do sistema sem prejudicar a agilidade e a eficiência do processo de desenvolvimento.
Agora vamos entender melhor o real valor desses testes no nosso dia a dia.
O Coração do Software!
Entender a importância de testar e validar os comportamentos na camada de domínio de um software é crucial para qualquer programador. Esta camada, onde as regras de negócio e a lógica são implementadas, é composta por entidades, objetos de valor e agregados. Cada um desses elementos desempenha um papel vital na representação e no funcionamento do domínio do negócio. Vamos mergulhar profundamente na importância dos testes sociáveis nesta camada e como eles podem revelar a integração e a interação entre diferentes classes.
A Camada de Domínio: Uma Visão Geral
Imagine a camada de domínio como uma orquestra sinfônica. Cada músico (entidade, objeto de valor, agregado) tem uma partitura específica para tocar. Individualmente, cada parte pode soar perfeita, mas o verdadeiro teste da orquestra está em como todas essas partes se harmonizam quando tocadas juntas. Da mesma forma, na camada de domínio, é crucial que as entidades e objetos de valor não apenas funcionem isoladamente, mas também em uníssono.
O Desafio com Testes Convencionais
Muitos programadores, ao testarem a camada de domínio, concentram-se em validar cada classe individualmente. Eles verificam se cada entidade e objeto de valor se comporta conforme esperado em um ambiente isolado. Embora isso seja importante, muitas vezes, eles perdem de vista como essas classes interagem entre si. É como se cada músico estivesse tocando sua parte sem ouvir os outros.
Para que fique um pouco mais claro esses desafios vamos conversar mais sobre testes solitários. E para que isso fique mais real possível vamos trabalhar com um exemplo concreto focado em uma classe de domínio crítica: Voucher
. Esta classe implementará regras de negócio essenciais para determinar a validade de um voucher. Além disso, vamos interagir com outra classe de domínio, Cliente
, que tem um papel fundamental na aplicação das regras do voucher.
public class Voucher {
private String code;
private LocalDate expiryDate;
private double discountPercentage;
private double minimumPurchaseAmount;
private int maximumUsage;
private int usageCount;
public Voucher(String code, LocalDate expiryDate, double discountPercentage, double minimumPurchaseAmount, int maximumUsage) {
this.code = code;
this.expiryDate = expiryDate;
this.discountPercentage = discountPercentage;
this.minimumPurchaseAmount = minimumPurchaseAmount;
this.maximumUsage = maximumUsage;
this.usageCount = 0;
}
public boolean isValidFor(Cliente cliente, double purchaseAmount) {
return isNotExpired() && isUnderUsageLimit() && meetsMinimumPurchaseAmount(purchaseAmount) && cliente.isEligibleForVoucher(this);
}
private boolean isNotExpired() {
return LocalDate.now().isBefore(expiryDate);
}
private boolean isUnderUsageLimit() {
return usageCount < maximumUsage;
}
private boolean meetsMinimumPurchaseAmount(double purchaseAmount) {
return purchaseAmount >= minimumPurchaseAmount;
}
public void redeem() {
usageCount++;
}
// Getters and Setters
}
Classe de Domínio Cliente
public class Cliente {
private String name;
private boolean isPremiumMember;
public Cliente(String name, boolean isPremiumMember) {
this.name = name;
this.isPremiumMember = isPremiumMember;
}
public boolean isEligibleForVoucher(Voucher voucher) {
// Regra de negócio: apenas membros premium podem usar certos vouchers
return isPremiumMember;
}
// Getters and Setters
}
Teste Solitário para Voucher
Agora, vamos criar um teste solitário para a classe Voucher
. Neste teste, vamos simular (mock) a interação com a classe Cliente
, focando apenas no comportamento isolado do Voucher
.
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import java.time.LocalDate;
public class VoucherTest {
@Test
public void testVoucherValidity() {
Cliente mockCliente = mock(Cliente.class);
when(mockCliente. (any(Voucher.class))).thenReturn(true);
Voucher voucher = new Voucher("ABC123", LocalDate.now().plusDays(10), 15.0, 100.0, 5);
assertTrue(voucher.isValidFor(mockCliente, 150.0), "Voucher should be valid for the given conditions");
}
}
Análise do Teste
O teste testVoucherValidity()
verifica se um Voucher
é considerado válido sob certas condições. Ele cria um mock da classe Cliente
e configura esse mock para sempre retornar true
quando o método isEligibleForVoucher()
é chamado. Em seguida, verifica se o Voucher
é válido para um determinado cliente e valor de compra.
Pontos Positivos
Isolamento de Teste: O teste isola a classe
Voucher
de suas dependências externas, permitindo que você verifique a lógica interna doVoucher
sem se preocupar com a implementação real deCliente
.Foco em Regras Específicas: Ele permite testar regras específicas dentro da classe
Voucher
, como a validade com base na data de expiração, no valor mínimo de compra e no limite de uso.
Limitações
Falta de Interação Real: Ao usar um mock para
Cliente
, o teste não verifica como umCliente
real interage com oVoucher
. Isso significa que ele não testa o comportamento completo do sistema, como seria em um ambiente de produção.Comportamento Simplificado: O teste assume que qualquer
Cliente
é elegível para oVoucher
, o que simplifica demais a realidade. Em um cenário real, a elegibilidade do cliente pode depender de vários fatores, como status de membro, histórico de compras, entre outros.Erros Ocultos nas Interações: O teste pode não capturar erros ou comportamentos inesperados que surgem quando
Voucher
eCliente
interagem de verdade. Por exemplo, se a lógica de determinar a elegibilidade do cliente for complexa ou se houver regras adicionais que afetam a validade do voucher.
Quero estender ainda mais essa analise indo mais afundo nas limitações que podemos encontrar e jamais gostariamos de deixar passar!
A Limitação do Teste Solitário
No teste acima, estamos verificando se o Voucher
é válido sob certas condições. No entanto, ao usar um mock para Cliente
, perdemos a oportunidade de testar como o Voucher
interage realmente com um Cliente
real. Por exemplo, a regra de que apenas membros premium podem usar certos vouchers é crucial e pode ter implicações significativas no comportamento do sistema. Em um teste solitário, essa interação complexa e crítica é simplificada, o que pode levar a uma falsa sensação de segurança. Mas não para por aí alguns problemas que podemos encontrar!
A limitação do teste solitário, especialmente na camada de domínio com regras de negócio complexas, vai além da incapacidade de testar interações reais entre classes. Existem várias outras nuances e desafios que podem surgir, tornando esses testes menos eficazes para garantir a robustez e a confiabilidade do software em cenários reais.
Limitações Adicionais dos Testes Solitários
1. Falha em Capturar Comportamentos Emergentes
Comportamentos Emergentes: Quando várias regras de negócio interagem, podem surgir comportamentos que não são evidentes ao testar cada regra isoladamente. Testes solitários podem falhar em identificar esses comportamentos emergentes, que só se tornam aparentes quando o sistema é testado como um todo.
2. Dificuldade em Testar Regras de Negócio Interdependentes
Regras Interdependentes: Em muitos sistemas de domínio, as regras de negócio não operam isoladamente; elas são interdependentes. Um teste solitário que zomba de uma dependência pode não ser capaz de validar adequadamente como as regras interagem e afetam umas às outras.
3. Problemas com a Validação de Estados Complexos
Estados Complexos: Alguns objetos de domínio podem ter estados internos complexos que são afetados por várias operações. Testes solitários podem não ser suficientes para verificar todos os possíveis estados e transições de estado, especialmente quando esses estados são influenciados por interações com outras classes.
Desafios com Boundaries e Erros Ocultos
1. Erros nas Fronteiras de Integração
Boundaries de Integração: As fronteiras entre diferentes objetos de domínio ou entre camadas de domínio e infraestrutura são pontos críticos onde erros podem se esconder. Testes solitários, ao focarem em uma única classe, muitas vezes não conseguem testar essas fronteiras de maneira eficaz.
2. Falha em Detectar Problemas de Integração
Problemas de Integração: Mesmo que cada classe passe nos testes solitários, podem existir problemas quando elas são combinadas. Por exemplo, incompatibilidades nas interfaces ou falhas na passagem de dados podem não ser detectadas em testes que não consideram a integração.
3. Limitações na Verificação de Contratos
Contratos entre Classes: Em um sistema orientado a objetos, as classes frequentemente definem "contratos" sobre como podem ser usadas ou como interagem com outras classes. Testes solitários podem verificar o cumprimento desses contratos de forma inadequada, especialmente quando o contrato envolve múltiplas partes.
O que podemos aprender então? Enquanto os testes solitários são valiosos para validar a funcionalidade de unidades individuais de código, eles têm limitações significativas quando se trata de regras de negócio complexas e interações entre classes na camada de domínio. Essas limitações podem levar a uma falsa sensação de segurança, onde os desenvolvedores acreditam que seu software é robusto e confiável, enquanto na realidade, podem existir falhas críticas não detectadas. Portanto, é crucial complementar os testes solitários com testes sociáveis ou outros métodos de teste de integração para garantir uma cobertura de teste abrangente e eficaz.
A Importância dos Testes Sociáveis
Aqui é onde os testes sociáveis entram em cena. Eles são como um ensaio geral da orquestra, onde todas as partes são tocadas juntas. Esses testes permitem que as entidades e objetos de valor interajam uns com os outros, revelando como eles se comportam em um cenário mais próximo do mundo real.
Entendendo a Integração das Classes
Quando escrevemos testes sociáveis, somos forçados a pensar sobre como diferentes classes estão integradas. Este tipo de teste proporciona uma visão mais abrangente e realista do comportamento do sistema, embora possa ser mais complexo em termos de implementação.
Vamos no exemplo anterior. Agora, vamos escrever um teste que utiliza instâncias reais dessas classes para verificar a interação entre elas.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import java.time.LocalDate;
public class VoucherIntegrationTest {
@Test
public void testVoucherWithRealClient() {
Cliente cliente = new Cliente("John Doe", true); // Cliente real
Voucher voucher = new Voucher("ABC123", LocalDate.now().plusDays(10), 15.0, 100.0, 5);
assertTrue(voucher.isValidFor(cliente, 150.0), "Voucher should be valid for a premium member with sufficient purchase amount");
}
}
Neste teste, estamos criando um Cliente
real e verificando se o Voucher
é válido para esse cliente sob condições específicas. Diferentemente do teste solitário, aqui não estamos usando mocks, mas sim trabalhando com as implementações reais das classes.
Pontos Positivos dos Testes Sociáveis
Interação Realista: Este teste reflete melhor como as classes
Voucher
eCliente
interagem no mundo real. Ele verifica se as regras de negócio são aplicadas corretamente quando as duas classes trabalham juntas.Detecção de Comportamentos Emergentes: Testes sociáveis podem revelar comportamentos que só surgem quando diferentes partes do sistema interagem, oferecendo uma visão mais completa do comportamento do sistema.
Validação de Regras de Negócio Complexas: Eles são particularmente úteis para validar regras de negócio complexas que dependem da interação entre várias classes.
Mas nem tudo são flores, eu poderia destacar isso mais para o final do artigo, mas acredito que trazer atona agora vai facilitar sua visão do outro lado da moeda sobre essa estratégia de teste.
Pontos Negativos dos Testes Sociáveis
Complexidade de Implementação: Testes sociáveis podem ser mais complexos para implementar, pois exigem a configuração de várias classes e suas interações.
Tempo de Execução: Eles podem levar mais tempo para serem executados, especialmente se o sistema for grande e as interações entre as classes forem complexas.
Manutenção: Manter testes sociáveis pode ser mais desafiador, pois mudanças em uma classe podem afetar os testes que envolvem várias classes.
Quero comentar mais sobre os pontos negativos com você:
Complexidade de Implementação
Por Que é um Ponto Negativo: Testes sociáveis exigem a configuração e o entendimento de várias classes e suas interações. Isso pode ser desafiador, pois requer um conhecimento profundo das relações e dependências dentro da camada de domínio. Além disso, simular cenários realistas de interação entre entidades e objetos de valor pode ser complexo.
Como Lidar: Para gerenciar essa complexidade, é útil adotar padrões de design e arquitetura claros na camada de domínio. Utilizar princípios como Injeção de Dependência e Segregação de Interfaces pode tornar as classes mais testáveis e as interações mais claras. Além disso, a documentação detalhada das regras de negócio e das interações pode ajudar a simplificar a implementação dos testes.
Tempo de Execução
Por Que é um Ponto Negativo: Testes sociáveis podem ser mais lentos para executar, pois envolvem a inicialização e interação de várias partes do sistema. Em sistemas grandes e complexos, isso pode resultar em um tempo de execução significativamente mais longo, o que pode afetar a agilidade do processo de desenvolvimento.
Como Lidar: Uma estratégia é otimizar os testes para executar apenas as interações necessárias. Isso pode incluir o uso de dados de teste focados e a limitação do escopo do teste às interações críticas. Além disso, a execução paralela de testes e a utilização de ambientes de teste eficientes podem ajudar a reduzir o tempo total de execução.
Desafios de Manutenção
Por Que é um Ponto Negativo: Mudanças em uma classe na camada de domínio podem afetar vários testes sociáveis que dependem dela. Isso pode tornar a manutenção dos testes mais trabalhosa, pois uma única alteração pode exigir a atualização de vários testes.
Como Lidar: Uma abordagem é modularizar o código da camada de domínio de forma que as mudanças em uma classe tenham impacto mínimo em outras partes. Além disso, a adoção de práticas como refatoração contínua e integração contínua pode ajudar a identificar e corrigir rapidamente os impactos das mudanças nos testes.
Embora os testes sociáveis na camada de domínio apresentem desafios em termos de quantidade de tempo dedicado, tempo de execução e manutenção, esses desafios podem ser gerenciados com práticas de desenvolvimento e design adequadas. Além disso podemos destacar seu papel fundamental em validar comportamentos críticos!
Profundezas dos Comportamentos Complexos em Testes
Os testes sociáveis são particularmente valiosos quando lidamos com comportamentos complexos na camada de domínio. Eles podem revelar problemas que só surgem quando diferentes partes do sistema interagem. Por exemplo, como o sistema lida com pedidos simultâneos de diferentes clientes? Há riscos de condições de corrida ou inconsistências de dados? Testes sociáveis podem ajudar a identificar e resolver esses problemas. Além disso podemos ampliar mais e comentar sobre como eles evitam cenários de testes falsos.
Para entender o conceito de falsos positivos e falsos negativos, vamos usar a analogia de um exame de COVID-19. Imagine que você faz um teste de COVID-19: um resultado falso positivo significa que o teste indica que você tem o vírus, quando na verdade não tem. Por outro lado, um falso negativo ocorre quando o teste indica que você não está infectado, mas na realidade, você está. Ambos os cenários são problemáticos: o falso positivo pode levar a preocupações e medidas desnecessárias, enquanto o falso negativo pode resultar em uma falsa sensação de segurança e na propagação inadvertida do vírus.
Agora, aplicando essa analogia aos testes sociáveis na programação, especialmente no contexto das classes Voucher
e Cliente
, podemos entender como esses testes ajudam a evitar falsos positivos e negativos.
Evitando Falsos Positivos
Um falso positivo em testes de software ocorre quando um teste indica que há um problema (falha) quando, na verdade, a funcionalidade está correta. Ou seja, o teste sinaliza erroneamente a presença de um erro.
Vamos considerar um cenário onde um teste solitário pode levar a um falso positivo:
Exemplo de Teste Solitário com Falso Positivo: Imagine que você está testando a lógica de um método na sua entidade de domínio
Voucher
. Este método determina se o voucher é válido ou não. No teste solitário, você cria um mock para um serviço (ou classe) que interage com oVoucher
.Vamos supor que configure esse mock para sempre retornar que o voucher é inválido, independentemente das condições reais do voucher ou das regras de negócio aplicáveis. Quando você executa o teste, ele falha, indicando que o voucher é sempre inválido, mesmo em situações onde, de acordo com as regras de negócio, ele deveria ser válido. Neste caso, o teste está produzindo um falso positivo. O erro não está na lógica de validação do voucher, mas na configuração do teste. O mock foi configurado de maneira inadequada, não refletindo a real complexidade e condições das regras de negócio do fluxo todo.
Contraste com Testes Sociáveis: Em um teste sociável, o
Voucher
seria testado em conjunto com uma instância real da classe que é uma dependência. Esse teste verificaria todas as regras e condições de negócio envolvidas na validação do voucher. Por exemplo, ele poderia verificar se o voucher é corretamente invalidado para clientes não elegíveis e após a data de expiração. Ao fazer isso, os testes sociáveis reduzem a chance de falsos positivos, pois eles estão testando o comportamento do sistema em condições mais realistas e complexas.
Minimizando Falsos Negativos
Um falso negativo em testes de software ocorre quando um teste não detecta um problema que realmente existe no software. Ou seja, o teste passa, indicando que tudo está funcionando corretamente, quando na verdade há um bug ou falha no sistema que o teste deveria ter capturado. Isso ocorre muito se configuramos os mocks de maneiras precipitadas ou erramos em algum ponto.
Agora vamos aos cenários:
Exemplo de Teste Solitário com Falso Negativo: Pense em um teste solitário para a classe
Voucher
que verifica se o voucher é inválido sob certas condições. Se esse teste for configurado com condições muito restritivas ou específicas (irrealistas), ele pode passar (indicando que o voucher é inválido) mesmo quando, na realidade, existem cenários de testes em que o voucher está válido, o que deveria ocasionar um erro no teste! Por exemplo, o teste pode não considerar todas as condições de elegibilidade de um Cliente real, levando a um falso negativo, o erro de lógica pode até existir mas não vai ser capturado com precisão.
Contraste com Testes Sociáveis: Em um teste sociável, o
Voucher
seria testado em interação real com uma classe, por exemploCliente
. Esse teste abrangente verificaria todas as condições e regras de negócio relevantes. Por exemplo, ele poderia validar se o voucher é corretamente aceito para clientes que atendem a todos os critérios de elegibilidade. Isso minimiza a ocorrência de falsos negativos, pois o teste está avaliando o comportamento do sistema em um contexto mais amplo.
Mas por que esses dois cenários são mais propensos de ocorrerem em testes solitários?
Esses cenários são particularmente propensos a ocorrer em testes solitários devido à natureza isolada desses testes. Vamos destacar as diferenças claras em relação aos testes sociáveis. 👇🏼
Falsos Positivos em Testes Solitários
Isolamento Excessivo: Testes solitários focam em uma única unidade ou classe, isolando-a de suas dependências reais. Eles frequentemente utilizam mocks ou stubs para simular o comportamento dessas dependências. Embora isso seja útil para testar a lógica interna da unidade, pode criar um cenário irrealista onde as interações complexas com outras partes do sistema não são testadas.
Falta de Verificação de Integração: Testes solitários não verificam como a unidade interage com outras partes do sistema. Isso significa que, mesmo que a unidade funcione perfeitamente isoladamente, ela pode falhar ou se comportar de maneira inesperada quando integrada com outras unidades ou no ambiente de produção.
Condições de Testes Restritivas: Em alguns casos, os testes solitários podem ser configurados com condições muito restritivas ou específicas que não representam adequadamente o uso real da unidade. Isso pode levar a falhas de teste, mesmo quando a unidade funcionaria corretamente em um cenário mais amplo ou integrado.
Mocks Inadequados: Se os mocks ou stubs usados nos testes solitários não imitarem com precisão o comportamento das dependências reais, isso pode levar a resultados de teste enganosos. Por exemplo, um mock pode não simular todas as possíveis respostas ou estados de uma dependência, resultando em um falso negativo.
Falta de Contexto do Sistema Maior: Testes solitários não consideram o contexto mais amplo do sistema. Uma unidade pode falhar em um teste solitário devido a suposições incorretas sobre seu ambiente operacional, enquanto na realidade, dentro do sistema maior, ela funcionaria como esperado.
Em contraste, os testes sociáveis abordam muitas dessas limitações:
Interações Reais: Eles permitem que unidades interajam com suas dependências reais, refletindo mais de perto o ambiente de produção, quando me refiro a produção, digo as interações reais ocorrendo.
Complexidade do Sistema: Testes sociáveis consideram a complexidade do sistema como um todo, testando como diferentes unidades e regras de negócio interagem e afetam umas às outras.
Contexto Mais Amplo: Eles fornecem um contexto mais amplo, verificando não apenas a funcionalidade isolada de uma unidade, mas também sua operação e integração dentro do sistema maior.
Portanto, enquanto os testes solitários são valiosos para focar em aspectos específicos de uma unidade de código, eles são mais propensos a falsos positivos e negativos devido ao seu isolamento e simplificação das condições de teste. Testes sociáveis, ao abordarem a interação e a integração das unidades dentro do contexto mais amplo do sistema, oferecem uma visão mais realista e abrangente, reduzindo a probabilidade desses cenários problemáticos.
Então sempre devo escolher testes sociáveis?
A escolha entre testes sociáveis e outros tipos de testes não é uma questão de "sempre" ou "nunca", mas sim de entender o contexto e as necessidades específicas do seu projeto. Enquanto os testes sociáveis têm seu valor, especialmente na camada de domínio, eles não são a solução universal para todos os cenários de teste. Vamos refletir um pouco sobre isso.
Perguntas para Reflexão
Qual é a Complexidade da Interação? Pergunte-se sobre a complexidade das interações entre as classes ou componentes que você está testando. Os testes sociáveis são mais valiosos quando há uma interação significativa e complexa que precisa ser validada.
Qual é o Impacto no Comportamento do Sistema? Considere se as funcionalidades que você está testando têm um impacto crítico e observável no comportamento do sistema. Se sim, os testes de unidade sociáveis que interagem com dependências reais podem ser a escolha certa.
Quão Críticas são as Funcionalidades para o Negócio? Avalie a importância das funcionalidades para o negócio. Em áreas críticas, onde erros podem ter consequências significativas, os testes sociáveis podem oferecer a profundidade de teste necessária.
Meu objetivo ao escrever esse artigo é trazer uma visão mais clara de como podemos trazer os testes sociáveis para nosso dia a dia e utilizar eles da maneira adequada para o benefício do software que estamos trabalhando.
Outras Camadas e Estratégias de Teste
Para outras camadas, como serviços, casos de uso e outras funcionalidades, outras estratégias e automações de testes podem ser mais adequadas. Por exemplo:
Services e Use Cases: Nestas camadas, onde a lógica de negócio pode ser menos complexa ou mais direta, testes utilizando mocks e stubs podem ser suficientes para validar o comportamento esperado.
Automações de Testes: Ferramentas como Cucumber e Pact são excelentes para testes de aceitação e contratos, respectivamente. Eles permitem que você escreva testes que são mais focados no comportamento do sistema do ponto de vista do usuário ou na comunicação entre diferentes partes do sistema.
Mas pode ser que você seja um leitor mais avançado e questione tudo o que comentei aqui. E isso é ótimo! Por isso deixe seu questionamento no comentário.
Uma dúvida que possa surgir é a seguinte:
Não estamos falhando com a resiliência de um teste, já que não estamos utilizando mocks para as dependências de suas classes?
Essa é uma pergunta interessante e importante. Ao discutir testes que interagem com suas dependências reais, principalmente na camada de domínio, é crucial entender que não estamos deixando nada de lado e também não significa abandonar as boas práticas de testes. Pelo contrário, mesmo em testes sociáveis, é essencial manter a resiliência, a clareza e a determinação.
Mantendo as Boas Práticas em Testes Sociáveis
Resiliência e Isolamento: Embora os testes sociáveis envolvam a interação entre diferentes partes do sistema, isso não significa que devemos ignorar a resiliência. Cada teste deve ser capaz de ser executado de forma independente, sem depender do resultado de outros testes. Isso ajuda a garantir que, se um teste falhar, você possa identificar rapidamente a causa do problema.
Determinismo: Testes sociáveis devem ser determinísticos. Isso significa que, dadas as mesmas entradas e condições, eles devem sempre produzir o mesmo resultado. Isso é crucial para a confiabilidade dos testes. Mesmo que os testes sociáveis envolvam interações complexas, eles devem ser projetados de forma a evitar flutuações inesperadas nos resultados.
Efeitos Colaterais Controlados: Em testes sociáveis, os efeitos colaterais - ou seja, as mudanças de estado que ocorrem como resultado das interações entre as classes - devem ser controlados e compreendidos. Isso não significa que os efeitos colaterais sejam indesejáveis; pelo contrário, eles são muitas vezes uma parte essencial da lógica de negócios que está sendo testada. No entanto, é importante que esses efeitos colaterais sejam previsíveis e consistentes.
Eficiência e Performance
Otimização de Tempo de Execução: Embora os testes sociáveis possam ser mais complexos, é importante otimizá-los para reduzir o tempo de execução, especialmente em sistemas grandes.
Seleção Criteriosa de Cenários de Teste: Escolha cuidadosamente os cenários de teste para cobrir os casos mais críticos e relevantes, evitando redundâncias.
Clareza e Objetividade
Descrição Clara: Cada teste sociável deve ter uma descrição clara do que está sendo testado e por quê. Isso ajuda outros desenvolvedores a entender rapidamente o propósito do teste.
Foco no Comportamento Observável: Os testes devem se concentrar em validar comportamentos que são significativos e observáveis, tanto do ponto de vista técnico quanto do usuário final.
Portanto, ao implementar testes sociáveis na camada de domínio, não estamos sugerindo uma abordagem desordenada ou descontrolada. Pelo contrário, estamos expandindo o escopo dos testes para abranger interações mais complexas e realistas, mantendo ao mesmo tempo a disciplina e as boas práticas de testes. Isso inclui garantir que os testes sejam resilientes, determinísticos e gerenciem adequadamente os efeitos colaterais.
As classes determinam que tipo de estrátegia de testes devemos utilizar?
A respostas direta é sim! Se o software segue uma arquitetura clara e limpa fica ainda mais fácil fazer essa distinção! Observe a imagem abaixo:
Olhando para este diagrama, você pode ver como ele segmenta claramente as responsabilidades de testes para as diferentes partes de um sistema. É como se cada tipo de classe tivesse um manual sugerindo como você deve abordar os testes.
Os Controllers estão ali na camada externa e, como você já sabe, eles precisam ser testados de forma isolada. Isso significa que você vai focar apenas na lógica que eles têm, garantindo que lidam bem com as requisições e as respostas sem se preocupar com as outras partes do sistema.
Da mesma forma, a Service é outra área que você testa de forma isolada. Você verifica se a lógica de aplicação que está dentro do serviço está fazendo exatamente o que deve fazer, sem interagir com o domínio ou a infraestrutura externa.
Agora, quando falamos do Domínio, que inclui as Entidades, os Objetos de Valor e as Sagas, o teste muda de figura. Você não está mais olhando para eles de forma isolada; ao invés disso, você verifica como eles se comportam quando trabalham juntos. Essas são as classes que realmente detêm as regras de negócios, então você quer ter certeza de que elas funcionam bem quando combinadas.
O diagrama deixa implícito que, mesmo que o foco aqui seja nos Controllers e nas classes do Domain, você provavelmente terá abordagens de testes semelhantes para os Adapters e o Repository. Os Adapters devem ser capazes de comunicar-se com os sistemas externos corretamente, enquanto o Repository deve interagir com o banco de dados sem problemas.
Então, a estratégia de teste é realmente definida pelo papel que a classe desempenha na arquitetura: se ela opera de maneira independente ou se ela é parte de uma coreografia mais complexa com outras classes.
Calma lá, estou quase concluindo esse artigo!😅
Gostaria de responder uma possível dúvida que eu tive e que muitos podem ter. 👇🏼
Por que incluir Sagas?
Esse tópico pode ser um pouco polêmico. Não vou me aprofundar tanto. Mas vou tentar resumir ao máximo meu ponto de vista.
Sagas em sistemas de software são mecanismos de gerenciamento de transações que lidam com operações complexas e de longa duração, normalmente envolvendo múltiplos passos e que podem abranger vários serviços e sistemas. Elas são diferentes de transações tradicionais porque cada passo pode envolver chamadas de rede, operações de banco de dados, ou outros processos que não são atômicos ou imediatos.
Por que precisam de testes sociáveis? Bem, sagas são projetadas para orquestrar fluxos de negócios que interagem com várias partes de um sistema. Isso significa que elas não estão apenas enviando comandos e esperando por eventos, mas também estão lidando com falhas, compensações e garantindo que o estado final de todo o processo de negócios seja consistente. Para testar isso efetivamente, você precisa simular as interações com as outras partes do sistema.
Imagine que temos uma saga que precisa validar um voucher antes de aplicar um desconto a um pedido. O teste sociável dessa saga precisaria simular as interações com o serviço de voucher, mas sem realmente tocar em um banco de dados ou um sistema de mensageria real.
public class ApplyVoucherSagaTest {
@Test
public void shouldApplyVoucherToOrder() {
given()
.saga(new ApplyVoucherSaga(voucherServiceProxy),
new ApplyVoucherSagaState(ORDER_ID, VOUCHER_CODE)).
expect().
command(new ValidateVoucher(VOUCHER_CODE, ORDER_ID)).
to(VoucherServiceChannels.voucherServiceChannel).
andGiven().
successReplyWithDiscount(DISCOUNT_AMOUNT).
expect().
command(new ApplyDiscountToOrder(ORDER_ID, DISCOUNT_AMOUNT)).
to(OrderServiceChannels.orderServiceChannel);
}
@Test
public void shouldRejectVoucherDueToValidationFailed() {
given()
.saga(new ApplyVoucherSaga(voucherServiceProxy),
new ApplyVoucherSagaState(ORDER_ID, VOUCHER_CODE)).
expect().
command(new ValidateVoucher(VOUCHER_CODE, ORDER_ID)).
to(VoucherServiceChannels.voucherServiceChannel).
andGiven().
failureReply().
expect().
command(new RejectVoucherApplication(ORDER_ID, VOUCHER_CODE)).
to(OrderServiceChannels.orderServiceChannel);
}
}
Neste teste, estamos usando proxies ou simulacros (stubs) para os serviços reais, como por exemplo kitchenServiceProxy.
Este é um objeto que foi configurado para responder a comandos específicos de uma maneira predeterminada, sem envolver chamadas a um serviço de cozinha real. Ele permite que o teste verifique se a saga está enviando os comandos corretos com base no estado do teste e nas respostas esperadas.1
Um teste solitário não seria adequado para o contexto do ApplyVoucherSagaTest
devido à natureza intrínseca das sagas e ao objetivo específico desse teste. Por que? Fazer isso seria focar exclusivamente na lógica interna da saga, isolando-a de todas as suas dependências externas. Isso significaria não simular ou testar as interações com serviços externos, como o serviço de validação de voucher ou o serviço de pedidos. No entanto, essas interações são críticas para verificar se a saga está cumprindo seu papel dentro do sistema. Sem testar essas interações:
Validação de Integração Falta: Você não seria capaz de verificar se a saga pode se comunicar corretamente com os serviços externos, o que é essencial para seu funcionamento.
Fluxo de Negócios Ignorado: Parte do papel da saga é gerenciar um fluxo de negócios que transcende os limites de uma única classe ou componente. Testes solitários não conseguem capturar essa orquestração entre diferentes componentes.
Reação a Eventos Externos: As sagas frequentemente precisam reagir a eventos ou respostas de serviços externos. Um teste solitário não pode verificar se a saga responde corretamente a esses estímulos externos.
Compensação e Gerenciamento de Falhas: Sagas muitas vezes incluem lógica para lidar com falhas ou compensar ações anteriores. Testes solitários não podem efetivamente simular essas condições de falha que dependem de interações com outros serviços.
Portanto, mesmo com as simulações em uso, o teste da saga ainda é sociável porque testa a integração e a interação da saga com as abstrações dos serviços que seriam usados na produção. A diferença é que estamos isolando o teste de variáveis externas e infraestrutura, o que aumenta a previsibilidade, a velocidade e a facilidade na hora de escrever e executar os testes. Por testar as sagas de maneira sociável, você se assegura de que a lógica complexa que elas encapsulam está funcionando como esperado dentro do ecossistema maior do aplicativo, mesmo quando essa interação é simulada por stubs ou proxies.
Conclusão
A chave para uma estratégia de teste eficaz é entender as necessidades e os desafios específicos do seu projeto. Enquanto os testes sociáveis são uma ferramenta poderosa na camada de domínio, eles são apenas uma parte de um arsenal mais amplo de técnicas de teste. Combinar diferentes abordagens de teste, adaptando-as às necessidades de diferentes partes do seu sistema, é a melhor maneira de garantir um software robusto e confiável.
Eu agradeço muito por ler o artigo! Fique à vontada para comentar e se foi útil por favor compartilhe! Grande abraço! 😉
Livros que considero fundamentais para uma mentalidade voltada a testes 👇🏼
Effective Software Testing: A Developer's Guide - by Mauricio Aniche
Unit Testing Principles, Practices, and Patterns - by Vladimir Khorikov
Microservices Patterns: With Examples in Java - by Chris Richardson
O objetivo dos proxies e stubs é simular essas interações de uma maneira controlada e previsível, permitindo que os testes se concentrem na lógica de negócios da saga sem as variáveis imprevisíveis de um ambiente de produção. Portanto, embora a saga não esteja conversando com os serviços reais de produção, ela está "socializando" com representantes deles, o que ainda exercita sua lógica de interação.
Isso não é uma contradição, mas sim uma adaptação do conceito de testes sociáveis para um ambiente de teste mais controlado, onde o que importa é a validação da lógica da saga e não a infraestrutura em si.