[{"content":"Esse artigo consolida todo o processo e conhecimento adquirido com o projeto como AI-Driven, desde a escolha do framework base do blog até o deploy no Github Pages.\nMotivação e Contexto do Experimento #Após passar as últimas semanas me aprofundando e entendendo os conceitos de Spec-Driven Development (SDD) e, principalmente, Harness Engineering, decidi colocar em prática com um pequeno projeto - chamado também de brownfield - que estava engavetado: meu próprio blog.\nAlém disso, a ideia da prática partiu inicialmente de um artigo do mestre Fábio Akita (aqui), no qual ele demonstra o uso da IA como assistente em um projeto real, contendo detalhes da implementação. E o projeto foi incrementado com docs de contexto criadas a partir de insights de um artigo do Eugênio (Gnios) com dicas de como podemos documentar contexto sobre o projeto para uso das IAs (aqui).\nEm resumo, esse projeto valida a tese sobre como é usar uma IA como assistente no dia a dia. A seguir, trago como foi discutir ideias através de brainstormings, montar specs com um fluxo personalizado bem simples de SDD e ajustar instruções para os agentes através do feedback dela mesma usando princípios do Harness Engineering.\nHands-on: O código-fonte resultante de todo esse experimento é o repositório aberto deste blog, disponível em luanmds/luanmds.github.io.\nTooling, Stack \u0026amp; Harness Customizado do Projeto #Antes de mergulharmos na metodologia e suas métricas, vamos destrinchar as tecnologias base e o harness customizado do projeto.\nStack utilizada # Hugo Framework: Para construção do corpo do site e o tema Congo com cores customizadas. Github Pages e Github Actions: para hospedar e realizar deploy, respectivamente. Tudo de forma gratuita do próprio Github. Para saber mais verifique essa documentação deles. CodeRabbit: AI Agent QA sintético - com free tier! - para revisar o código gerado em Pull Requests abertos por outros agentes no repositório. Configurado através do arquivo .coderabbit.yaml Playwright: Ferramenta/Library utilizada para automatizar testes funcionais em páginas web. No projeto, tem o papel de validar alterações no front-end antes de seguir com commit e merge. OpenCode: AI Agent via terminal, com foco em codificação e uso de ferramentas. Utilizado como parceiro de codificação principal. A maior vantagem é a capacidade de usar diversos modelos LLM e suas skills. É orquestrado pelo harness do projeto para seguir o workflow SDD. Por ser agnóstico a modelos, foi possível testar com diferentes LLMs diferentes, como Claude Sonnet, Claude Haiku e GPT-5.3 Codex. Harness Feedforward # AGENTS.md: Principal arquivo com todo o resumo do projeto e um manual de bordo para os agentes seguirem o workflow configurado para o projeto. .docs/: Pasta com informações detalhadas do contexto relacionado ao projeto. Possui todas as diretivas e cada arquivo é mapeado no AGENTS.md. specs/: Pasta com as especificações desenvolvidas e implementadas. Cada especificação possui um arquivo tasks.md onde é listado tudo o que deve ser feito para que consideremos a implementação como Done. Abaixo, um print de como ficou o harness organizado no projeto:\nA Metodologia Spec-Driven e Orquestracão de Agentes #Algo que aprendi estudando o Harness é que entender melhor o ciclo de vida é fundamental para o alinhamento inicial e, principalmente, o que pode ou não ser feito.\nNo Spec-Driven Development, a especificação é o pilar que guia todo o processo de criação de conteúdo. Iniciando por um brainstorming, passando por uma etapa de especificação, decomposição em tarefas e, finalmente, a implementação. Aqui percebi que a especificação se torna o artefato principal desse processo, diferente do desenvolvimento tradicional onde o código é o artefato principal.\nO Ciclo de Vida da Entrega #O fluxo de trabalho foi dividido em etapas claras e interdependentes:\n1. Planejamento # Brainstorming: Uso da brainstorming-skill (do Superpowers Skills) para validar decisões arquiteturais e de stack antes do primeiro commit. Especificação (Spec): Criação de arquivos Markdown na pasta specs/ (operando em modo PLAN) detalhando o comportamento esperado e as restrições técnicas. Validação do Usuário: Etapa obrigatória (Verify and Validate) onde a especificação é revisada, refinada e aprovada pelo \u0026ldquo;Product Owner\u0026rdquo; (o próprio usuário) antes de qualquer código ser gerado. 2. Execução # Decomposição em Tarefas: Tradução da Spec aprovada para um arquivo tasks.md com itens atômicos, paralelizáveis e com Definition of Done (DoD) bem definida. Implementação: Fase onde os agentes de IA assumem a execução das tarefas e escrevem o código sob a supervisão restrita do Harness. Testes com Playwright: Validação visual e funcional da entrega utilizando a playwright-skill rodando localmente (via Docker) antes de seguir para o commit. 3. Qualidade e Deploy # Pull Request e CodeRabbit: Empacotamento das mudanças em um PR, engatilhando a revisão de código automatizada do CodeRabbit AI. Deploy Contínuo: Publicação automatizada via GitHub Actions enviando a versão validada para o GitHub Pages. flowchart TD %% Phase 1 subgraph Planning direction LR B[Brainstorming Skill] --\u003e S[Spec Creation] S --\u003e V{User Validation} V -- Refine --\u003e S end %% Phase 2 subgraph Execution direction LR T[Task Decomposition] --\u003e I[Implementation] I --\u003e P[Playwright Tests] P -- Fail --\u003e I end %% Phase 3 subgraph QD[Quality \u0026 Deploy] direction LR PR[Create Pull Request] --\u003e CR[CodeRabbit AI Review] CR -- Issues Resolved --\u003e D[Deploy to GH Pages] end Planning -- Approved --\u003e Execution Execution -- Pass --\u003e QD Casos de Uso em Destaque #Durante o tempo do experimento, alguns cenários destacaram, na prática, o potencial e a flexibilidade dessa abordagem.\n1. A Sessão Gigante: Construindo a Base (Tema PaperMod + Multilingual) #Esta foi a mais densa de todo o projeto. Com duração de 285 minutos de tempo ativo real e 671 mensagens trocadas, o agente foi responsável por gerar a base completa do tema PaperMod no blog e todo o sistema de troca de idioma. O mais impressionante foram os números: ~73,9 milhões de tokens foram consumidos nesta única sessão, resultando em um saldo de +904 linhas de código adicionadas. Desse total de tokens, incríveis 97% foram lidos diretamente do cache do contexto já estabelecido.\nA atuação do CodeRabbit (QA Sintético)\nApesar da geração massiva de código, os agentes autônomos podem cometer deslizes em detalhes estruturais. Neste Pull Request #3 que implementou as mudanças, o CodeRabbit identificou três pontos críticos de revisão:\nNotou que o build via Docker na pipeline estava rodando como root, sugerindo a flag --user $(id -u):$(id -g) para evitar artefatos com problemas de permissão. Alertou que o submódulo do tema estava atrelado a um commit de desenvolvimento instável e recomendou fixar na release tag oficial. Cobrou a refatoração da lógica de internacionalização no front-end: em vez de usar ifs soltos e fixados nos templates ({{ if eq .Lang \u0026quot;pt\u0026quot; }}), orientou registrar essas strings nos arquivos de idioma corretos e utilizar chaves i18n. 2. Spec 007: Migração do tema PaperMod para Congo com Subagents Paralelos #Um outro caso notável foi a especificação Spec 007 (Migração do tema PaperMod para Congo). O trabalho durou 71 minutos, com uma alteração concentrada de +253 linhas e remoção de 229 linhas, e um consumo total de ~10,6 milhões de tokens.\nEm vez de uma execução linear, apliquei o padrão Subagent-Driven Development: o agente orquestrador disparou 8 sub-agentes paralelos. Cada sub-agente assumiu uma tarefa independente (cores, tipografia, estrutura de menus), todos operando simultaneamente sobre a mesma fonte da verdade (a Spec). Isso permitiu uma migração complexa em tempo recorde, com consistência arquitetural garantida.\nA atuação do CodeRabbit (QA Sintético)\nNo Pull Request #11 focado na paleta de cores (Crimson Circuitry), o CodeRabbit agiu exigindo consistência nos padrões adotados:\nLocalizou um débito técnico sutil: 7 valores de cores no formato numérico puro de rgba() esquecidos no custom.css. Ele exigiu que fossem substituídos pela invocação correta das variáveis CSS do nosso design system (--color-primary-*). Além da revisão de código, ele leu as regras do Harness e revisou ativamente o cumprimento documentado da tarefa, exigindo que as caixas de seleção da própria Spec (arquivos .md) fossem atualizadas para \u0026ldquo;CONCLUÍDO\u0026rdquo;. Análise de Métricas do OpenCode #Os dados abaixo foram extraídos diretamente do banco de dados do OpenCode (via arquivo opencode.db) cobrindo o período do projeto. No total, houveram 25 sessões (~12,3 horas de tempo ativo real), que modificaram 96 arquivos e geraram um saldo líquido de +1.891 linhas de código (+2.765 adicionadas, -874 removidas).\nAbaixo o detalhamento das métricas de processamento:\nMétrica Quantidade % do total Total de Tokens Contabilizados ~141,7 milhões 100% Cache Reads (reutilizado) ~134,2 milhões 94,7% Cache Writes (gravado) ~4,35 milhões 3,1% Output (gerado pelo modelo) ~548 mil 0,4% Reasoning (raciocínio oculto) ~308 mil 0,2% Input genuinamente novo ~2,26 milhões 1,6% A Mágica do Cache e Custo Zero\nO dado mais revelador desse experimento foi os 94,7% dos tokens serem lidos do cache (Context Efficiency). É interessante notar que o agente mantém o contexto completo \u0026ldquo;quente\u0026rdquo; (arquivos, documentação, histórico) a cada mensagem enviada, mas não precisa reprocessar o que já está cacheado.\nIsso explica como foi possível consumir 141,7 milhões de tokens sem custo adicional usando a assinatura do GitHub Copilot. O consumo real de inferência (input novo + reasoning + output) foi de apenas ~3,1 milhões de tokens.\nSessões Principais e Produtividade\nA tabela abaixo mostra a distribuição de esforço e saldo de código nas principais sessões do projeto:\nSessão (Foco) Tempo Ativo Saldo de Linhas Tokens Totais Project Base (HuGo + PaperMod + Multilingual) 285 min +904 / -111 73,9M Migração de artigos (arquivos .doc no Google Drive) 166 min +0 / -0 23,2M Atualizar página Sobre/About 73 min +36 / -38 8,8M Migração Tema Congo (Spec 007) 71 min +253 / -229 10,6M Ajustes finais + update de specs 39 min +447 / -214 5,9M Responsividade/favicon/Tags 33 min +25 / -16 5,7M README.md 32 min +144 / -16 7,1M Coleta de contexto 16 min +804 / -198 3,6M Configure code automation 14 min +101 / -2 1,5M Observação: A sessão de \u0026ldquo;Migração de artigos\u0026rdquo; levou 166 minutos e processou 23 milhões de tokens sem modificar nenhuma linha de código no repositório final. Isso ocorreu pois o conteúdo e as imagens foram processados fora do controle de versão (geração de conteúdo bruto em massa).\nResumo das Atividades por Sessão:\nProject Base: Sessão mais densa. O agente configurou a base completa do blog com o Hugo, o tema inicial PaperMod e a infraestrutura de internacionalização (PT/EN) com chave de tradução. Migração de artigos: Sessão longa focada em processar textos de rascunho (via Google Drive/Medium) e formatá-los para markdown com front matter adequado. Atualizar página Sobre/About: Criação e atualização de conteúdo específico para a página Sobre/About, como foto de perfil, histórico e ajustes de design pontuais. Migração Tema Congo (Spec 007): Execução da Spec 007, orquestrando 8 sub-agentes paralelos para migrar as cores e layout do tema antigo para o Congo, ajustando tipografia e menus simultaneamente. Ajustes finais + update de specs: Revisão de templates, padronização do formato dos artefatos na pasta specs/ e refinamentos antes do deploy final. Responsividade/favicon/Tags: Ajustes finos de UI/UX, garantindo navegação responsiva, o favicon correto e a exibição de tags nas postagens. README.md: Geração do arquivo público do repositório, extraindo o contexto diretamente da documentação interna. Coleta de contexto: Sessão dedicada a gerar a documentação base na pasta .docs/, mapeando stack, arquitetura e estabelecendo o AGENTS.md a partir do estado atual. Configure code automation: Configuração inicial de linting, CI/CD e integração do CodeRabbit (QA sintético de Pull Requests). Modelos LLM Utilizados nas sessões principais # Modelo Sessões Total contabilizado Cache Read Cache Write Real processado gpt-5.3-codex 5 ~90,2M ~87,5M (96%) — ~2,7M claude-sonnet-4.6 20 ~47,9M ~43,4M (90%) ~4,1M ~387K claude-haiku-4.5 1 ~4,9M ~4,4M (91%) ~368K ~36K Observação: Real processado = input novo + output + reasoning — o que o modelo de fato inferiu.\nConclusão #Após colocar a versão 1.0 do projeto em produção (https://luanmds.github.io), listei algumas conclusões e lições aprendidas ao longo do processo:\nMudança de mindset como Dev: O desenvolvedor passa a ser um \u0026ldquo;Designer de Contexto\u0026rdquo; e \u0026ldquo;Orquestrador de Agentes\u0026rdquo;. O que não é ruim - no meu ponto de vista - mas exige um aprendizado de como interagir com as IAs para extrair o máximo de benefício. Mas ainda é preciso entender o que a IA está gerando e ter um conhecimento sólido sobre Arquitetura e Design de Software para garantir que o software mantenha um nível de qualidade aceitável.\nDocumentação é a chave para uma boa experiência: À medida que a GenAI avança, a capacidade de interagir com ela de forma eficaz se torna uma habilidade crítica. A qualidade da documentação influencia diretamente a capacidade da IA de entender o contexto do projeto e gerar respostas relevantes e precisas.\nA importância do AGENTS.md como artefato central de documentação, ou seja, um guia que o Agente sempre levar consigo ao interagirmos com ele. O SDD, independente de usar um framework ou ferramenta específica (como o OpenCode), se mostra a melhor forma de documentar um projeto. Isso porque ele se baseia no conceito de \u0026ldquo;documentação do que precisa ser feito\u0026rdquo; ao invés de \u0026ldquo;documentação do que foi feito\u0026rdquo;. Processo de Harness é o mais importante no processo de uso da IA como assistente: Sem ele, a IA tem dificuldade em entender o contexto do projeto e gerar respostas relevantes e precisas. Por isso, é importante estar sempre revisando o input usado pela IA (Feedforward) e o que ela retorna como resposta (Feedback) para que consiga refinar o contexto do projeto.\nParafraseando o mestre Fábio Akita: \u0026ldquo;Faça um bom prompt, teste, veja o que ele retornou. Se não ficou bom, conserte seu prompt e refaça\u0026rdquo;.\nIsso é tudo pessoal… #Gostou do relato ou tem alguma dúvida de como apliquei esses conceitos na prática? Deixe um comentário lá no repositório ou me chame nas redes. O feedback de vocês é sempre bem-vindo!\nReferências #SDD, Harness Engineering \u0026amp; Context Engineering # Spec-Driven Development: AI Assisted Coding Explained Understanding Spec-Driven-Development: Kiro, spec-kit, and Tessl — Martin Fowler The ONLY guide you\u0026rsquo;ll need for GitHub Spec Kit AI-Assisted Development # AI-Assisted Coding Tutorial – OpenClaw, GitHub Copilot, Claude Code, CodeRabbit, Gemini CLI Do Zero à Pós-Produção em 1 Semana — Fábio Akita Antes de qualquer ferramenta: como documentar seu projeto para a IA — Gnios How do thinking and reasoning models work? Large Language Models Survey — arxiv.org ","date":"9 mai. 2026","permalink":"https://luanmds.com/posts/engenharia-blog-sdd-harness/","section":"Posts","summary":"Um relato técnico sobre como este blog foi construído utilizando desenvolvimento assistido por IA, validando os limites de SDD e Harness Engineering.","title":"A Engenharia por trás deste blog: SDD e Harness em um workflow 100% IA"},{"content":"","date":null,"permalink":"https://luanmds.com/categories/","section":"Categories","summary":"","title":"Categories"},{"content":"Engenharia de software com foco em backend, arquitetura distribuida e IA aplicada ao dia-a-dia dev.\n","date":null,"permalink":"https://luanmds.com/","section":"Luan Mello","summary":"","title":"Luan Mello"},{"content":"","date":null,"permalink":"https://luanmds.com/posts/","section":"Posts","summary":"","title":"Posts"},{"content":"","date":null,"permalink":"https://luanmds.com/categories/software-engineering/","section":"Categories","summary":"","title":"Software-Engineering"},{"content":"","date":null,"permalink":"https://luanmds.com/categories/testes/","section":"Categories","summary":"","title":"Testes"},{"content":"Nesse artigo vamos abordar de forma compilada os conceitos, definições e boas práticas gerais em Testes de Integração. No detalhe, entenderemos em quais cenários os testes de integração se encaixam, como implementar de forma fácil de manter, além de ver quando não usar mocks em seus cenários. Compreender isso lhe capacita a tomar decisões relacionadas a testes automatizados e a medir o esforço necessário antes mesmo de implementar, usando qualquer linguagem de programação ou framework.\nHands-on: Para consolidar esses estudos, desenvolvi o repositório tests-dotnet-best-practices. Lá, você encontrará alguns exemplos de implementação prática incluindo todos os conceitos que discutiremos neste artigo!\nBoa leitura!\nPirâmide de testes e onde a integração entra # A famosa pirâmide de testes ordena os tipos de testes que um software pode realizar, sempre do mais barato (o Unit Tests na base) para o mais caro (E2E no topo). Os testes de integração estão ali no meio, acima dos testes de unidade, lidando com 20% a 30% da suíte de testes.\nOs testes de integração ocupam uma camada intermediária, sendo responsáveis por verificar a conexão, interação e os contratos entre componentes, módulos ou serviços. Além de expor problemas a nível de sistema e garantir uma alta cobertura servindo de um feedback importante antes de cada deploy.\nVale comentar que os testes de integração não garantem 100% de cobertura e deve ser utilizado em conjunto com outros testes - como o de unidade, mas remove aquela “pulga atrás da orelha” respondendo perguntas como: “Se eu atualizar esse módulo, vai quebrar nos módulos dependentes?” ou “Como eu garanto que esse fluxo continue funcionando quando o componente X não estiver disponível?”.\nAbordagens para testar integrações #Para escolher uma abordagem é necessário entender como estão acoplados os componentes da aplicação e qual o nível de complexidade para isolar falhas dos fluxos que os envolvem.\nDica: Comece com fluxos críticos onde deve-se garantir que funcionarão em diversas situações e cenários.\nOs tipos de abordagens são:\nBig Bang: Todos os módulos são integrados simultaneamente e testados como um todo. Embora pareça rápido, é a abordagem mais arriscada, pois torna a depuração extremamente difícil quando um erro ocorre, já que a falha pode estar em qualquer lugar da cadeia. Incremental: Abordagens focadas em testar um conjunto de módulos da aplicação. Seguem elas: - Top-Down: O teste começa pelas camadas superiores (como Controllers ou APIs) e desce em direção às camadas de infraestrutura. Utiliza-se Stubs para substituir os módulos inferiores que ainda não foram integrados. - Bottom-Up: Inicia-se pelos módulos de baixo nível (como Repositórios e Drivers de banco de dados) e sobe para as camadas de lógica de negócio. É excelente para validar a persistência de dados logo no início do desenvolvimento. - Sanduíche (Híbrida): Combina as vantagens das abordagens Top-Down e Bottom-Up, testando o núcleo do sistema enquanto as camadas periféricas são integradas gradualmente. Escopo de Teste #Para uma visão mais moderna e aplicada a microsserviços e sistemas distribuídos, é fundamental diferenciar o alcance do teste:\nNarrow Integration Tests (Estreitos): Focam apenas na comunicação entre o serviço e um componente externo específico (ex: um repositório e o SQL Server). O restante do sistema é substituído por dublês de teste (test doubles). São mais rápidos e fáceis de manter. Broad Integration Tests (Amplos): Validam a integração de todos os componentes vivos que compõem uma funcionalidade, cruzando diversas camadas e serviços. Eles exigem um ambiente mais complexo, mas garantem que o fluxo completo de ponta a ponta esteja íntegro. Cenários ideais para o uso #Nem toda funcionalidade exige um teste de integração. O valor desses testes reside na validação de fluxos que cruzam as fronteiras da aplicação. Os cenários onde eles são indispensáveis incluem:\n1. Comunicação com Infraestrutura de Persistência e Cache #Cenários onde a aplicação interage com bancos de dados (SQL ou NoSQL) e sistemas de cache (como Redis). Garantindo que as queries, mapeamentos de ORM (como Entity Framework), migrações e restrições de integridade (chaves estrangeiras, índices) funcionem conforme o esperado no motor de banco de dados real.\n2. Integração com Serviços de Mensageria #Cenários que envolvem a publicação e o consumo de eventos em Message Brokers (RabbitMQ, Azure Service Bus, Kafka). Validando se a serialização dos objetos está correta, se as filas/tópicos estão configurados adequadamente e se a aplicação reage corretamente a falhas de conexão ou retentativas.\n3. Consumo de APIs Externas e Web Services #Quando o sistema depende de APIs de terceiros (Gateways de pagamento, serviços de CEP, etc.). Permitindo validar se o contrato da API externa ainda é respeitado e como o seu sistema lida com diferentes códigos de status HTTP (400, 401, 500) e timeouts. Aqui também pode conter alguns testes de contrato.\n4. Fluxos de Negócio Críticos #Processos core que não podem deixar de funcionar e atravessam diversos serviços ou domínios dentro da mesma aplicação. Por exemplo, um processo de checkout que envolve componentes relacionados a: estoque, pagamento e logística. Nesses fluxos, os testes de unidade costumam \u0026ldquo;mockar\u0026rdquo; as dependências, o que pode esconder bugs de lógica que surgem apenas no encadeamento real das chamadas.\nBoas práticas para seguir #Implementar testes de integração exige mais do que apenas código; exige uma estratégia de ambiente e uma compreensão clara de onde o risco reside. A seguir, deixo boas práticas, quase obrigatórias (para mim são sempre obrigatórias rs) para garantir uma melhor implementação e manutenção dos testes:\nIdentificando componentes e o SUT #Antes de tocar no teclado, o passo fundamental é mapear e diagramar todos os componentes do sistema. Incluindo o SUT (System Under Test). No contexto de integração, o SUT geralmente é a sua API ou um serviço específico que precisa interagir com o \u0026ldquo;mundo externo\u0026rdquo;. Com isso, você terá uma visão ampla das fronteiras de infraestrutura e dependências externas.\nFoque onde o código realiza operações de entrada e saída (E/S), como bancos de dados, APIs de terceiros, microserviços e filas de mensagens. Também olhe em fluxos com componentes que possuem um alto acoplamento, esses são os mais frágeis com toda certeza! Se sua arquitetura utiliza padrões como Adapters ou Camadas de Repositório, estes são os alvos primários para garantir que a tradução de dados entre o seu domínio e o mundo externo esteja correta.\nAs técnicas BDD (Behaviour-Driven Design) e Event Storming, ajudam a mapear com facilidade. E para identificar o nível de acoplamento de cada componente, podemos utilizar as denotações de Acoplamento Aferente e Eferente.\nCriando um ambiente mais próximo de Produção #A utilidade de um teste de integração está diretamente ligada à sua fidelidade. A melhor prática moderna é o uso de containerização (via Docker) para simular a infraestrutura real. Isso permite que você execute os testes contra motores de banco de dados e brokers de mensageria idênticos aos de produção, aumentando drasticamente a chance de encontrar erros reais de configuração ou comportamento.\nAlém disso, integre esse ambiente à sua pipeline de CI. Embora nem toda mudança exija a execução completa da suíte — especialmente se não houver alteração em módulos de integração — ter essa rede de segurança automatizada é o que impede regressões no ambiente final.\nTest Doubles e por que não usar em fluxos críticos #Os dublês de teste (Test Doubles) são ferramentas poderosas para isolamento e performance. Eles são ideais em cenários de dependências lentas, inacessíveis ou quando você precisa simular falhas difíceis de reproduzir (como timeouts de rede).\nMocks: Para validar comportamentos e interações. Stubs: Para fornecer respostas prontas e simples. Fakes: Para implementações funcionais, porém simplificadas (como um SQLite em memória). Contudo, existe uma armadilha: evite dublês em fluxos críticos. Um banco de dados \u0026ldquo;fake\u0026rdquo; pode ter comportamentos de sensibilidade a maiúsculas ou transações complexas diferentes do seu banco real, o que pode mascarar bugs fatais. Para o núcleo do seu negócio, a validação de alta fidelidade contra recursos reais é inegociável para garantir um deploy seguro\nTeste de Contrato através da integração #Em ecossistemas distribuídos e arquiteturas de microsserviços, garantir que quem consome e quem provê a informação falem exatamente a mesma língua é vital. O Teste de Contrato surge para resolver o problema dos testes de integração que se tornam lentos ou instáveis demais por dependerem de muitos serviços ativos simultaneamente.\nEsse teste foca na estrutura das mensagens e protocolos de comunicação, sendo mais leve e rápido por não exigir todo o ecossistema ativo, podendo ser executado como um teste à parte dentro da sua estratégia de integração, utilizando ferramentas como o Pact para validar a compatibilidade entre APIs e mensageria de forma eficiente.\nIsso é tudo pessoal… #Se chegou até aqui, você percebeu que escrever testes de integração não é apenas sobre aumentar o percentual de cobertura de código. Ao entender as fronteiras da sua aplicação, escolher a abordagem correta para cada escopo e aplicar ferramentas modernas para simular o mundo real (como contêineres reais em vez de apenas mocks), você eleva drasticamente a resiliência do seu sistema.\nA teoria é o mapa, mas a prática é o caminho. Não deixe de conferir o repositório tests-dotnet-best-practices para ver como aplicar esses conceitos no código real usando .NET 9, Testcontainers, Aspire e outras coisitas mais!\nSe este conteúdo foi útil para você, me mande um feedback e compartilhe com seu time.Aliás, aqui vai uma pergunta: quais são os seus maiores desafios ao criar testes de integração nos seus projetos? Deixe nos comentários, vamos trocar experiências!\nReferências # https://martinfowler.com/bliki/IntegrationTest.html Integration Testing - Engineering Fundamentals Playbook Integration Testing - Software Engineering - GeeksforGeeks ASP.NET Core Integration Testing Tutorial https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests Choosing a testing strategy - EF Core | Microsoft Learn https://coupling.dev/posts/related-topics/afferent-and-efferent-coupling/ https://behave.readthedocs.io/en/latest/philosophy/ ","date":"28 mar. 2026","permalink":"https://luanmds.com/posts/testes-integracao-visao-geral/","section":"Posts","summary":"Conceitos, definições e boas práticas em Testes de Integração — quando usar, como estruturar sua suíte e quando evitar mocks.","title":"Testes de Integração: Visão Geral e Boas Práticas"},{"content":"Boas práticas em Unit Tests com .Net: A prática #Essa é a parte dois do artigo sobre Boas práticas em Unit Tests com .Net contendo a parte prática da teoria apresentada na primeira parte.\nCaso não tenha lido, recomendo ler a Primeira Parte do artigo com a teoria.\nCriando testes de unidade em .Net #Começamos com o framework xUnit .Net, voltado para simplificar o desenvolvimento de testes de unidade em .Net em diversas linguagens como C#, F# e demais linguagens .Net. É open source e mantido pela .Net Foundation.\nO xUnit utiliza uso de Decorators indicando, assim, quais métodos ou funções dentro de um arquivo serão parte do conjunto de testes ao executá-los.\nMock e Fixtures #Para agilizar a geração de Mocks e instâncias de objetos necessários para a realização de um teste, temos as libraries Moq e AutoFixture.\nO Moq, cria objetos “fakes” como Mocks e Stubs, que auxiliam bastante o processo de instanciar e gerenciar dependências do teste.\nJá o AutoFixture ( Quick Start ) , cria um objeto ou um Array de objetos através do padrão Builder para qualquer classe que é necessária uma instância, estando um estado específico ou não, colocando um valor aleatório em cada uma propriedade sua de acordo com a tipagem.\nExemplos práticos #O contexto para os exemplos está nesse repositório no Github.\nUsando o [Fact] #Nesse primeiro exemplo, temos testes unitários para o método Create da classe CustomerService.\nUsamos o Decorator [Fact] do xUnit para indicar ao SDK do .Net que tal método é um teste de unidade e executá-lo. Além disso, nosso “Stub” é gerado pela classe Fixture da lib AutoFixture.\npublic class BasicTestsWithFactDecorator { private readonly Mock\u0026lt;ICustomerRepository\u0026gt; _repositoryMock; private readonly Mock\u0026lt;ILogger\u0026lt;CustomerService\u0026gt;\u0026gt; _loggerMock; private readonly Fixture _fixture; private readonly CustomerService _service; public BasicTestsWithFactDecorator() { _repositoryMock = new Mock\u0026lt;ICustomerRepository\u0026gt;(); _loggerMock = new Mock\u0026lt;ILogger\u0026lt;CustomerService\u0026gt;\u0026gt;(); _fixture = new Fixture(); _service = new CustomerService(_repositoryMock.Object, _loggerMock.Object); } [Fact] public async Task Save_customer_successfully_when_Dto_is_valid() { var dto = new Mock\u0026lt;CustomerDto\u0026gt;(); await _service.Create(dto.Object); dto.Verify(x =\u0026gt; x.ToCustomer(), Times.Once); _repositoryMock.Verify(x =\u0026gt; x.SaveAsync(It.IsAny\u0026lt;Customer\u0026gt;()), Times.Once); _loggerMock.Verify(l =\u0026gt; l.Log(LogLevel.Information, It.IsAny\u0026lt;EventId\u0026gt;(), It.Is\u0026lt;It.IsAnyType\u0026gt;((v, _) =\u0026gt; v.ToString().Contains(\u0026#34;Customer save successfully\u0026#34;)), It.IsAny\u0026lt;Exception\u0026gt;(), It.IsAny\u0026lt;Func\u0026lt;It.IsAnyType, Exception, string\u0026gt;\u0026gt;()), Times.Once); } [Fact] public void Save_customer_fails_when_repository_throws_exception() { var dto = new Mock\u0026lt;CustomerDto\u0026gt;(); _repositoryMock.Setup(x =\u0026gt; x.SaveAsync(It.IsAny\u0026lt;Customer\u0026gt;())).Throws\u0026lt;Exception\u0026gt;(); Func\u0026lt;Task\u0026gt; result = async () =\u0026gt; await _service.Create(dto.Object); result.Should().ThrowAsync\u0026lt;Exception\u0026gt;(); dto.Verify(x =\u0026gt; x.ToCustomer(), Times.Once); } [Fact] public async Task GetById_returns_customer_successfully_when_id_is_valid() { string id = _fixture.Create\u0026lt;string\u0026gt;(); var customer = _fixture.Build\u0026lt;Customer\u0026gt;().With(x =\u0026gt; x.Id, id).Create(); _repositoryMock.Setup(x =\u0026gt; x.GetAsync(It.IsAny\u0026lt;string\u0026gt;())) .ReturnsAsync(customer); var result = await _service.GetById(id); result.Should().BeOfType\u0026lt;Customer\u0026gt;(); _repositoryMock.Verify(x =\u0026gt; x.GetAsync(It.IsAny\u0026lt;string\u0026gt;()), Times.Once); } [Fact] public void GetById_throws_ArgumentNullException_when_id_is_not_found() { string id = _fixture.Create\u0026lt;string\u0026gt;(); Func\u0026lt;Task\u0026gt; result = async () =\u0026gt; await _service.GetById(id); result.Should().ThrowAsync\u0026lt;ArgumentNullException\u0026gt;(); _repositoryMock.Verify(x =\u0026gt; x.GetAsync(It.IsAny\u0026lt;string\u0026gt;()), Times.Once); } } Aqui usa-se a classe Mock (da lib Moq) para abstrair o acesso ao ICustomerRepository e Logger. O método Setup de Mock é usado para selecionar qual será o valor ou retorno ao chamar uma propriedade ou método da classe \u0026ldquo;mockada\u0026rdquo;.\nTambém é possível fazer em N vezes para uma mesma chamada com o método Setups.\nUsando o Theory #Nesse segundo exemplo temos o uso do Decorator [Theory] juntamente com o [InlineData()] que geram diversos tipos de entradas como parâmetros e, assim, conseguirmos realizar o teste em diversos cenários.\npublic class UsingTheoryWithInlineData { private readonly Mock\u0026lt;ICustomerRepository\u0026gt; _repositoryMock; private readonly Mock\u0026lt;ILogger\u0026lt;CustomerService\u0026gt;\u0026gt; _loggerMock; private readonly Fixture _fixture; private readonly CustomerService _service; public UsingTheoryWithInlineData() { _repositoryMock = new Mock\u0026lt;ICustomerRepository\u0026gt;(); _loggerMock = new Mock\u0026lt;ILogger\u0026lt;CustomerService\u0026gt;\u0026gt;(); _fixture = new Fixture(); _service = new CustomerService(_repositoryMock.Object, _loggerMock.Object); } [Theory] [InlineData(ProcessStatus.Pending, \u0026#34;123\u0026#34;)] [InlineData(ProcessStatus.Blocked, \u0026#34;456\u0026#34;)] [InlineData(ProcessStatus.Processed, \u0026#34;789\u0026#34;)] public async Task UpdateStatusScore_a_customer_successfully_when_parameters_are_valid(ProcessStatus status, string customerId) { var customer = _fixture.Build\u0026lt;Customer\u0026gt;().With(x =\u0026gt; x.Id, customerId).Create(); _repositoryMock.Setup(x =\u0026gt; x.GetAsync(It.IsAny\u0026lt;string\u0026gt;())). ReturnsAsync(customer); _repositoryMock.Setup(x =\u0026gt; x.UpdateAsync(It.IsAny\u0026lt;Customer\u0026gt;())). ReturnsAsync(customerId); await _service.UpdateStatusScore(status, customerId); _repositoryMock.Verify(x =\u0026gt; x.GetAsync(It.IsAny\u0026lt;string\u0026gt;()), Times.Once); _repositoryMock.Verify(x =\u0026gt; x.UpdateAsync(It.IsAny\u0026lt;Customer\u0026gt;()), Times.Once); _loggerMock.Verify(l =\u0026gt; l.Log(LogLevel.Information, It.IsAny\u0026lt;EventId\u0026gt;(), It.Is\u0026lt;It.IsAnyType\u0026gt;((v, _) =\u0026gt; v.ToString().Contains($\u0026#34;Customer with ID: {customerId} update successfully\u0026#34;)), It.IsAny\u0026lt;Exception\u0026gt;(), It.IsAny\u0026lt;Func\u0026lt;It.IsAnyType, Exception, string\u0026gt;\u0026gt;()), Times.Once); } [Theory] [InlineData(ProcessStatus.Pending, \u0026#34;123\u0026#34;)] public void UpdateStatusScore_throws_ArgumentNullException_when_id_is_not_found(ProcessStatus status, string customerId) { Func\u0026lt;Task\u0026gt; result = async () =\u0026gt; await _service.UpdateStatusScore(status, customerId); result.Should().ThrowAsync\u0026lt;ArgumentNullException\u0026gt;(); _repositoryMock.Verify(x =\u0026gt; x.GetAsync(It.IsAny\u0026lt;string\u0026gt;()), Times.Once); } } Aqui temos dois cenários de teste onde o valor do enum ProcessStatus varia e gera um valor de resposta específico. Onde cada InlineData se torna um cenário a ser testado.\nEsse tipo de abordagem nos ajuda demais já que economiza boas linhas de código para fazer um teste de unidade para cada cenário existente daquele comportamento.\nMemberData e ClassData para reúso de código #O próximo exemplo utiliza o [MemberData] com o intuito de termos como passar objetos via parâmetro de forma dinâmica.\nPara utilizá-lo corretamente é obrigatório utilizar a interface IEnumerable\u0026lt;object[]\u0026gt; e fornecer uma lista de objetos utilizados como inputs necessários para o teste funcionar. Segue o exemplo:\npublic class UsingTheoryWIthMemberData { private readonly Mock\u0026lt;ICustomerRepository\u0026gt; _customerRepositoryMock; private readonly Mock\u0026lt;IScoreRepository\u0026gt; _repositoryMock; private readonly Mock\u0026lt;ILogger\u0026lt;ScoreService\u0026gt;\u0026gt; _loggerMock; private readonly Fixture _fixture; private readonly ScoreService _service; public UsingTheoryWIthMemberData() { _repositoryMock = new Mock\u0026lt;IScoreRepository\u0026gt;(); _customerRepositoryMock = new Mock\u0026lt;ICustomerRepository\u0026gt;(); _loggerMock = new Mock\u0026lt;ILogger\u0026lt;ScoreService\u0026gt;\u0026gt;(); _fixture = new Fixture(); _service = new ScoreService(_repositoryMock.Object, _customerRepositoryMock.Object, _loggerMock.Object); } [Fact] public async void CreateScore_successfully_when_customer_exists() { var customer = _fixture.Create\u0026lt;Customer\u0026gt;(); _customerRepositoryMock.Setup(x =\u0026gt; x.GetAsync(customer.Id)) .ReturnsAsync(customer); await _service.CreateScore(customer.Id); _customerRepositoryMock.Verify(x =\u0026gt; x.GetAsync(customer.Id), Times.Once); _repositoryMock.Verify(x =\u0026gt; x.SaveAsync(It.IsAny\u0026lt;Score\u0026gt;()), Times.Once); _loggerMock.Verify(l =\u0026gt; l.Log(LogLevel.Information, It.IsAny\u0026lt;EventId\u0026gt;(), It.Is\u0026lt;It.IsAnyType\u0026gt;((v, _) =\u0026gt; v.ToString().Contains(\u0026#34;Score with ID:\u0026#34;)), It.IsAny\u0026lt;Exception\u0026gt;(), It.IsAny\u0026lt;Func\u0026lt;It.IsAnyType, Exception, string\u0026gt;\u0026gt;()), Times.Once); } [Fact] public void CreateScore_throws_ArgumentNullException_when_customer_not_found() { var id = _fixture.Create\u0026lt;string\u0026gt;(); Func\u0026lt;Task\u0026gt; result = async () =\u0026gt; await _service.CreateScore(id); result.Should().ThrowAsync\u0026lt;ArgumentNullException\u0026gt;(); _customerRepositoryMock.Verify(x =\u0026gt; x.GetAsync(It.IsAny\u0026lt;string\u0026gt;()), Times.Once); _repositoryMock.Verify(x =\u0026gt; x.SaveAsync(It.IsAny\u0026lt;Score\u0026gt;()), Times.Never); } [Theory] [MemberData(nameof(CalculateScoreData))] public void CalculateScore_should_return_correct_value_to_the_debits_amount(decimal debits, int expectedScore) { int score = ScoreService.CalculateScore(debits); score.Should().Be(expectedScore); } public static IEnumerable\u0026lt;object[]\u0026gt; CalculateScoreData() { yield return new object[] { 2000f, 0 }; yield return new object[] { 1000.0f, 0 }; yield return new object[] { 500.3f, 50 }; yield return new object[] { 450f, 55 }; yield return new object[] { 250.9f, 75 }; yield return new object[] { 0f, 100 }; } } Nele vemos que podemos ter diversos cenários para cálculo do Score de um Customer de acordo com os débitos do mesmo.\nNo último exemplo temos o [ClassData] sendo bem parecido com o anterior, mas fornece os dados via uma classe implementando a interface IEnumerable\u0026lt;object[]\u0026gt;. Veja mais:\npublic class UsingTheoryWithClassData { private readonly Mock\u0026lt;ILoanRepository\u0026gt; _repositoryMock; private readonly Mock\u0026lt;IScoreRepository\u0026gt; _scoreRepositoryMock; private readonly Fixture _fixture; private readonly LoanService _service; public UsingTheoryWithClassData() { _repositoryMock = new Mock\u0026lt;ILoanRepository\u0026gt;(); _scoreRepositoryMock = new Mock\u0026lt;IScoreRepository\u0026gt;(); _fixture = new Fixture(); _service = new LoanService(_repositoryMock.Object, _scoreRepositoryMock.Object); } [Fact] public async void CreateLoan_successfully_when_score_exists() { var score = _fixture.Create\u0026lt;Score\u0026gt;(); var dt = DateTime.UtcNow; _scoreRepositoryMock.Setup(x =\u0026gt; x.GetAsync(score.Id)) .ReturnsAsync(score); await _service.CreateLoan(score.Id, dt); _scoreRepositoryMock.Verify(x =\u0026gt; x.GetAsync(score.Id), Times.Once); _repositoryMock.Verify(x =\u0026gt; x.SaveAsync(It.IsAny\u0026lt;Loan\u0026gt;()), Times.Once); } [Fact] public void CreateLoan_throws_ArgumentNullException_when_score_not_found() { var id = _fixture.Create\u0026lt;string\u0026gt;(); var dt = DateTime.UtcNow; Func\u0026lt;Task\u0026gt; result = async () =\u0026gt; await _service.CreateLoan(id, dt); result.Should().ThrowAsync\u0026lt;ArgumentNullException\u0026gt;(); _scoreRepositoryMock.Verify(x =\u0026gt; x.GetAsync(It.IsAny\u0026lt;string\u0026gt;()), Times.Once); _repositoryMock.Verify(x =\u0026gt; x.SaveAsync(It.IsAny\u0026lt;Loan\u0026gt;()), Times.Never); } [Theory] [ClassData(typeof(CalculateLoanValueData))] public void CalculateLoanValue_should_return_correct_value(decimal debits, int score, decimal expectedLoan) { decimal result = _service.CalculateLoanValue(debits, score); result.Should().Be(expectedLoan); } } public class CalculateLoanValueData : IEnumerable\u0026lt;object[]\u0026gt; { public IEnumerator\u0026lt;object[]\u0026gt; GetEnumerator() { yield return new object[] { 2000f, 0, 0M }; yield return new object[] { 450f, -55, 0 }; yield return new object[] { 500.3f, 50, 4499.7 }; yield return new object[] { 250.9f, 75, 7249.1 }; yield return new object[] { 0f, 100, 10000 }; } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } } Mesmo que os Decorators sejam parecidos, temos uma clara diferença entre eles. E isso justamente ajuda no tipo de abordagem utilizado para reaproveitar o código.\nNesse caso, podemos reaproveitar os dados gerados pela classe CalculateLoanValueData, propagando entre outras classes de testes de unidade.\nDeixando mais claro o resultado esperado com Fluent Assertions #Podemos substituir o conhecido Assert.{method} de nossos testes em .Net pela library Fluent Assertions.\nEsse conjunto de extensions é utilizado para validar asserções de forma intuita e legível nos métodos de testes de unidade.\nNo exemplo abaixo, é testado cada campo retornado na variável result do método ToCustomer:\npublic class CustomerDtoTests { private readonly Fixture _fixture; public CustomerDtoTests() { _fixture = new Fixture(); } [Fact] public void ToCustomer_Should_Returns_Customer_With_DTO_Data() { var sut = _fixture.Create\u0026lt;CustomerDto\u0026gt;(); var result = sut.ToCustomer(); result.Address.Street.Should().Be(sut.Street); result.Address.City.Should().Be(sut.City); result.Address.StreetNumber.Should().Be(sut.StreetNumber); result.Name.Should().Be(sut.Name); result.Age.Should().Be(sut.Age); result.Document.Should().Be(sut.Document); result.DebitsAmount.Should().Be(sut.DebitsAmount); } } Referências # Mocking em testes unitários com o framework Moq GitHub — moq/moq4: Repo for managing Moq 4.x Introduction Quick Start — AutoFixture ","date":"19 jun. 2025","permalink":"https://luanmds.com/posts/unit-tests-dotnet-pratica/","section":"Posts","summary":"Parte prática sobre boas práticas em Unit Tests com .Net — criando testes com xUnit, Moq e NSubstitute aplicando os fundamentos na prática.","title":"Boas práticas em Unit Tests com .Net: A Prática"},{"content":"Essa é a parte um do artigo sobre Boas práticas em Unit Tests com .Net contendo os fundamentos e a base de estudo com o intuito de entender o que é um teste de unidade, saber diferenciar o teste bom do teste ruim e como é a anatomia do teste bom.\nCaso já tenha lido este artigo, siga para a Segunda Parte com a prática.\nUnit test, o teste do Dev #Teste de unidade ou teste unitário é uma categorização da pirâmide de testes automatizados (criada por Mike Cohn). Pode-se considerá-lo como o mais importante, pois é a base dessa pirâmide.\nPor ser a primeira camada da pirâmide, é o teste mais próximo do código, e é esperado que seja o mais isolado e de execução rápida quanto possível. Se tornando uma responsabilidade do próprio Desenvolvedor.\nComo seu próprio nome diz, foca em realizar testes de forma unitária com uma granularidade própria e isolando os comportamentos de uma aplicação ou sistema.\nAntes de continuar apresento duas siglas importantes nos testes unitários: SUT e MUT.\nSUT ou System Under Test é o QUEM estamos testando do sistema. Podendo ser qualquer parte dele, como uma classe ou um conjunto de classes que geram um comportamento.\nMUT ou Method Under Teste é o QUE estamos testando do sistema. Representa o comportamento do SUT que queremos testar nossos cenários.\nTestes Ruins #Para classificarmos um teste de unidade como ruim veja o exemplo abaixo:\n[Fact] public void test_when_data_null() { var x = new Customer() {Document = null}; Assert.False(x.IsValidDocument(),\u0026#34;Document is not valid\u0026#34;); } Nesse exemplo, nota-se que aparentemente é um teste simples e curto mas precisamos até ler duas vezes para entender melhor, ou por vezes, nem conseguimos entender.\nPodemos ver que o nome do método test_when_data_null é confuso, não tendo nenhuma coesão com o que está sendo testado, além disso não se sabe ao certo para que serve o teste e como está sendo testado, sendo perigoso alterar algo e quebrá-lo!\nCom isso chegamos as principais características de um teste unitário ruim:\nO alto custo de manutenabilidade. Já que custa tempo e esforço do profissional para codar e entender o que está feito; Gerar falsos alarmes. Ou seja, este quebrar sem mudança significativa no código; Dificuldade em refatoração. Com o mesmo efeito do anterior mas podendo ser difícil alterar a si mesmo; Inelegibilidade do código; Testes Bons #Um bom teste de unidade segue esses três princípios básicos:\nVerifica o comportamento do sistema, ou seja, uma unidade; Executa rapidamente, mas vai do tempo satisfatório para o desenvolvedor; E é executado de maneira isolada, de forma a não afetar outros testes unitários; Veja o código a seguir:\n[Fact] public async void CreateScore_successfully_when_customer_exists() { var customer = _fixture.Create\u0026lt;Customer\u0026gt;(); _customerRepositoryMock.Setup(x =\u0026gt; x.GetAsync(customer.Id)) .ReturnsAsync(customer); await _service.CreateScore(customer.Id); _customerRepositoryMock.Verify(x =\u0026gt; x.GetAsync(customer.Id), Times.Once); _repositoryMock.Verify(x =\u0026gt; x.SaveAsync(It.IsAny\u0026lt;Score\u0026gt;()), Times.Once); } Esse é um bom exemplo pois segue os princípios acima e cobre os problemas apresentados no teste ruim. Nele, é possível entender o objetivo do teste, as regras que estão sendo testadas e as asserções que precisam ser validadas.\nAlém disso, tem-se vantagens adicionais como:\nProteção contra regressões no sistema #Com o teste focado apenas no necessário, priorizando o domínio e os comportamentos mais críticos do sistema, evita-se regressões no código assim que os testes unitários forem executados, antes do deploy da aplicação.\nPara evitar regressões é preciso evitar testes em Libraries, Frameworks e sistemas externos, esse último se encaixa como um teste de integração.\nA dica é colocar esses recursos como apenas dependências dos testes unitários.\nFeedback rápido de seu status #Testes focados apenas no necessário, simulando o comportamento esperado e alcançando seu resultado, conseguem executar de forma mais rápida e responder se houve sucesso ou falha.\nResistência na Refatoração #Esse tipo de teste deve evitar ao máximo uma refatoração em si causada por uma refatoração ou mudança no código do domínio.\nRefatoração que quebra um teste unitário pode demonstrar um falso positivo — teste falha mas continua cobrindo o comportamento e seu resultado. Isso vai do desenvolvedor entender melhor o que está sendo testado!\nFonte: Unit Testing Principles, Practices, and Patterns de Vladimir Khorikov\nDica: Foque os testes nos resultados gerados em chamadas do método (MUT) no SUT e não em cada passo realizado de forma interna para gerar o resultado.\nA Entropia dos testes #A medida que adicionamos novos recursos no código-base, acrescentamos complexidade e até desorganização nele.\nEm outras palavras, ele se deteriora com o tempo e acontece um efeito chamado Entropia (assim como na termodinâmica). Mas testes de unidade sendo bem escritos e uma refatoração constante no código de produção auxiliam na diminuição do efeito.\nFonte: Unit Testing Principles, Practices, and Patterns de Vladimir Khorikov\nA forma do teste de unidade bom #O teste de unidade bom segue padrões que o torna sólido, legível, organizado e dinâmico. A seguir demonstra-se os mais utilizados:\nPadrão AAA — Arrange, Act and Assert #Esse padrão é utilizado para organizar qual comportamento será testado, suas dependências e como é validado o resultado.\nArrange é a primeira etapa e consiste em organizar seus objetos, criá-los e alimentá-los com valores iniciais — ou o que for mais necessário — para que o teste funcione; Act é, literalmente, a ação ou execução do objeto para obter um valor a ser testado. Evite utilizar mais de uma ação ou ato no teste; Assert a última etapa, responsável por realizar o teste de unidade em si e retornar um resultado deste; O Given_When_Then também ajuda a entender o fluxo do teste de uma outra maneira mas que alcance o mesmo resultado; Dica: Evite utilizar condicionais IF dentro do código do teste. Além de não alcançar os princípios anteriormente ditos, demonstra que o teste está realizando alguma ação a mais que não seja necesssária.\nNomenclatura de um unit test #Um bom padrão que auxilia na nomenclatura é:\n[MethodUnderTest]_[Scenario]_[ExpectedResult]\nMethodUnderTest: O nome do método que está sendo testado. Scenario: O cenário em que ele está sendo testado. ExpectedResult: O comportamento esperado quando o cenário é invocado. Exemplo: Sum_WithTwoValidNumbers_ReturnsSum ou Sum_With_Two_ValidNumbers_ReturnsSum Dica: Não é necessário seguir a risca esse padrão pois o que se espera é algo legível e que fique de forma clara o objetivo do teste para o desenvolvedor. Um bom exemplo é: Sum_of_two_numbers\nReuso de código entre testes #Quando um conjunto de testes utiliza dependências ou inicia instâncias de forma idêntica na etapa do Arrange — chamamos de Test Fixtures — já pode se pensar em reutilizar o código, abstraindo-o com alguns padrões.\nUsando método privado #Já deve reduzir bastante a quantidade de código e reaproveitar instanciações de uma mesma classe e um conjunto de testes unitários comum.\nNesse cria-se um método privado na própria classe/arquivo dos testes de unidade que inicializa e dispõe de objetos comuns. Segue o exemplo:\npublic class CustomerTests{ [Fact] public void Purchase_succeeds_when_enough_inventory() { Store store = CreateStoreWithInventory(Product.Shampoo, 10); Customer sut = CreateCustomer(); // generate a no-data Customer bool success = sut.Purchase(store, Product.Shampoo, 5); Assert.True(success); Assert.Equal(5, store.GetInventory(Product.Shampoo)); } [Fact] public void Purchase_fails_when_not_enough_inventory() { Store store = CreateStoreWithInventory(Product.Shampoo, 10); Customer sut = CreateCustomer(); // generate a no-data Customer bool success = sut.Purchase(store, Product.Shampoo, 15); Assert.False(success); Assert.Equal(10, store.GetInventory(Product.Shampoo)); } private Store CreateStoreWithInventory(Product product, int quantity) { Store store = new Store(); store.AddInventory(product, quantity); return store; } private static Customer CreateCustomer() { return new Customer(); } } Teste Data Builder #Técnica que utiliza-se do padrão Builder para gerar e montar um objeto útil no teste.\nContinuando nosso exemplo anterior, podemos ter uma classe auxiliar para criar um Customer:\npublic class CustomerBuilderStub { private long CustomerId; private string Document; private int Age; private Coupon Coupon; public CustomerBuilderStub withId(long customerId) { this.CustomerId = customerId; return this; } public CustomerBuilderStub withDocument(string document) { this.Document = document; return this; } public CustomerBuilderStub withAge (int age) { this.Age = age; return this; } public CustomerBuilderStub withCouponCode(Coupon coupon) { this.Coupon = coupon; return this; } public Customer build() { Customer customer = new Customer(CustomerId, Document, Age, Coupon); return customer; } } Mocks e Stubs, os ajudantes do teste unitário #Grande parte das vezes quando se está codificando um teste de unidade, é necessário criar uma falsificação de uma dependência do SUT que estamos testando pois é importante que a dependência responda de uma maneira esperada para o próprio teste.\nDe forma geral, chamamos essa “falsificação” de Test Double, ou dublê de testes. Há basicamente 2 grandes grupos de Test Double são eles: Mocks e Stubs.\nMocks #São objetos configurados para atender expectativas que formam chamadas específicas com o objetivo de auxiliar nas interações futuras que alteram o estado do SUT.\nStubs #Objetos que fornecem respostas enlatadas para chamadas feitas durante o teste, geralmente não respondendo a nada fora do que está programado para o teste. Focam em auxiliar nas interações recebidas quando o SUT chama suas dependências e obtem dados de entrada (input). Exemplos clássicos são: Banco de Dados, criação complexa do objeto de uma dependência, algum Service, etc.\nMocks podem ser Stubs #Um Mock pode agir como um Stub onde ele é usado para alterar o estado de um SUT e no mesmo teste tem papel de ser um gerador de dado de entrada para auxiliar um método dentro do próprio SUT.\n[Fact] public void Purchase_fails_when_not_enough_inventory() { var storeMock = new Mock\u0026lt;IStore\u0026gt;(); // configura uma resposta fechada storeMock.Setup(x =\u0026gt; x.HasEnoughInventory(Product.Shampoo, 5)).Returns(false); var sut = new Customer(); bool success = sut.Purchase(storeMock.Object, Product.Shampoo, 5); Assert.False(success); // verifica a chamada de um SUT storeMock.Verify(x =\u0026gt; x.RemoveInventory(Product.Shampoo, 5), Times.Never); } Referências # Melhores práticas para escrever testes de unidade — .NET Unit Testing Principles, Practices, and Patterns — Vladimir Khorikov Código C# de teste de unidade no .NET Core usando dotnet test e xUnit — .NET Test Data Builders: você está usando corretamente? How to Create a Test Data Builder | Code With Arho Mocks Aren’t Stubs ","date":"19 jun. 2025","permalink":"https://luanmds.com/posts/unit-tests-dotnet-teoria/","section":"Posts","summary":"Fundamentos e teoria dos Unit Tests com .Net — entenda o que é um bom teste de unidade, sua anatomia e os princípios F.I.R.S.T.","title":"Boas práticas em Unit Tests com .Net: A Teoria"},{"content":"Neste artigo, demonstrarei como devemos lidar com cenários de transações ao usar Saga Pattern e realizar rollbacks destas entre microserviços de forma compilada juntamente com um exemplo abordando os principais tópicos desse padrão. O objetivo é usar esse artigo como consulta e revisão no futuro, aguardo seu feedback e boa leitura!\nA ideia da Saga #A ideia do uso de Sagas iniciou de um problema específico em sistemas que utilizam transações longas e/ou sequenciais (chamadas originalmente de Long Lived Transactions ou LLT) com operações envolvendo atomicidade em banco de dados e interação com outros sistemas. A forma mais comum da LLT é distribuir transações, ou seja, realizar operações em diversos serviços ou bases de dados que manipulam dados relacionados.\nO padrão Saga amadureceu bastante com o tempo e hoje possui características auxiliares às arquiteturas modernas de sistemas distribuídos, base de dados como NoSQL e message brokers na qual devem lidar com a consistência dos dados e encontrar o equilíbrio seguindo o Teorema CAP. Antes de apresentar esse pattern, vamos entender o problema das transações distribuídas!\nProblemas com Transações Distribuídas e ACID #Atualmente, encontramos arquiteturas de microserviços em sistemas modernos utilizando transações distribuídas seguindo o modelo ACID. Essa abordagem possui bastante limitações e pode acarretar problemas ao sincronizar dados e desfazer operações por conta de alguma indisponibilidade ou apenas por cancelamento de um processo.\nO primeiro problema se torna aparente justamente na dependência de um microserviço realizar uma operação que necessita do inicio de outra operação em outro microserviço e o mande uma requisição. O segundo problema aparece numa das principais características do microserviço: arquitetura única, ou seja, são construídos seguindo padrões como o modelo de database-per-microservice, onde tem-se a liberdade de selecionar o tipo do banco de dados e como persiste os dados, além de se comunicar de uma forma própria (via mensageria ou protocolo HTTP, por exemplo). Isso complica ainda mais na implementação do modelo ACID por conta do grande gerenciamento a cada transação realizada e, caso alguma falhe, avisa as demais que houve um problema e estas tomam providências.\nHá outras maneiras de manter a consistência de dados com transações distribuídas além do Saga — já o apresento mais abaixo, prometo — como os protocolos inter-process, two-phase commit, Try-Confirm-Cancel (TCC), entre outros. O foco desse artigo é o Saga por ser o mais utilizado e adequado a maioria das soluções e arquiteturas modernas de software.\nO Padrão Saga #A saga é um fluxo representado com uma sequência de transações seguindo uma certa ordem de forma síncrona e/ou assíncrona. Essas transações não são distribuídas e, por isso, cada transação é local. Após confirmar o fim da transação, chama a próxima e assim sucessivamente até o fim da Saga. Podemos ter um identificador da saga para cada transação no encadeamento e obter um rastreamento do fluxo inteiro.\nOrquestração e Coreografia #A saga pode ser implementada seguindo dois modelos atuais:\nCom um Orquestrador, também conhecido como Saga Execution Coordinator (SEC), controlando cada transação da Saga e orientando qual será a próxima transação ou desfazer commits de transações passadas quando alguma transação ocorrer uma falha (falaremos disso a seguir). Assim como um maestro e sua orquestra, sabe sempre o próximo passo e quem deve realizá-lo.\nNesse exemplo simples, temos um Broker que gerencia, consolida e provê toda a comunicação entre os 3 serviços. A orquestração tem o lado ruim de depender do centralizador. Nesse caso, se o broker estiver fora do ar, todas os outros serviços e apps estarão também. Ou seguindo o modelo de Coreografia, assim como em um Flash mob (eu devo estar velho!) cada integrante - ou um microserviço, no nosso caso - conhece como e quando deve realizar a sua parte aguardando ou não a vez de outro integrante. Voltando para nossa realidade, os microserviços conhecem a Saga e sabem aonde vão atuar e iniciar sua transação local.\nNo exemplo abaixo, temos um fluxo que envolve 4 sistemas (A, B, C e D) e possui 3 etapas a serem realizadas. Em cada etapa, os sistemas agem através de uma ação recebida, nesse caso usamos um broker de mensageria e chamada HTTP. A desvantagem disso, é a administração e monitoramento de cada sistema para garantir o funcionamento de ponta a ponta.\nDesafios ao implementar o padrão #Independente da abordagem implementada, e também em qualquer outro padrão e tecnologia , haverão obstáculos a serem considerados e entram na balança do Trade-Off.\nSegue uma lista com alguns possíveis problemas:\nImplementação da observabilidade de forma mais detalhada para cada etapa do fluxo de uma Saga; Haverão pontos de falha. E entender como revertê-los em cada aplicação é essencial (detalho mais sobre com um exemplo a seguir); Debugar e rastrear uma entidade ou agregado pode ser mais difícil por conta de ter diversas na saga. Por isso um identificador da Saga e uma coordenação entre as aplicações durante todo o fluxo se faz necessário; Grande probabilidade do uso de Eventos (procure por Event-Driven Architecture) no fluxo da Saga, aumentando ainda mais a complexidade e obrigando a ter um maior controle do que é enviado e recebido em tópicos e filas; Exemplificando com Caso de uso #Para exemplificar o uso da Saga, usamos um caso de uso ficticio envolvendo três microservicos em uma área de Marketing. Segue um resumo do uso de cada uma:\nO primeiro é um serviço de criação de Posts via API usadas por um funcionário de Marketing, o Post Service. O segundo é um serviço de envio de e-mail para clientes externos quando houver um novo Post e se comunica via mensageria, o Notification Service. O último é um serviço externo que recebe o post via mensageria e persiste em um banco de dados para um WebSite externo, o WebSiteService. A Saga se inicia na criação de um Post de marketing (com promoções, cupons, etc) que deve ser notificado aos clientes e atualizado no site.\nAbaixo, segue o fluxo de sucesso da Saga completa:\nTudo muito bonito! Maaas, e se, digamos, ao criar um novo Post, a notificação é feita aos clientes via e-mail mas não é atualizada no site. Temos uma falha crítica e devemos tratá-la, a seguir demonstro como lidarmos com esse problema.\nLidando com falhas em uma Saga #Padrão Retry (Retentativas) #Quando há uma falha, podemos seguir retentando um passo da Saga por uma certa quantidade de vezes combinada com um intervalo de tempo, até termos certeza da ocorrência da falha e ela não será resolvida automaticamente. Essa abordagem de retentar alguma ação chamamos de Retry Pattern.\nNo fluxo acima, imagine a situação em que a mensageria ficou offline por alguns segundos e a mensagem para o serviço do WebSite não foi enviada (passo 3). Assumindo que o Post Service possui uma política de retentativas das mensagens tentando por uma quantidade de vezes a cada x minutos, temos o seguindo fluxo:\nMuito Bom né? o serviço realizou diversas tentativas até a mensageria estar no ar novamente e não precisamos ter um alerta gritando por aí. Mas…e quando temos certeza da falha, mesmo depois de aplicar todas as políticas de retentativa? Precisamos realizar alguma outra ação para resolver, ou até desfazer, a nossa Saga. A resposta pode estar nas transações compensatórias!\nTransações Compensatórias #Ok, houve uma falha no nosso fluxo impedindo a continução da Saga. Devemos compensar esse erro e desfazer as ações anteriores.\nPara isso, o padrão Saga possui as Transações Compensatórias como um mecanismo indicador para que uma transação feita seja…desfeita além de avisar outras aplicações para desfazerem as suas transações localmente também. Com isso, seguimos com nosso caso de falha crítica onde clientes receberam as notificações de novas promoções e descontos mas não há nenhuma promoção no site!! Os próximos passos são desfazer e mandar uma errata para eles como uma compensação. Então, ajustamos o ponto no fluxo onde ocorre o erro e chegamos a uma resolução:\nOs passos 3a e 3b fazem parte da transação compensatória assim que o passo 2 é realizado e há a falha.\nDiminuindo Rollbacks #Também podemos evitar que falhas e compensações aconteçam simplesmente analisando os pontos de falha e reorganizando o processo, também é possível revendo requisitos funcionais e não-funcionais.\nRevendo todo o fluxo, evita-se a falha apresentada apenas ajustando o envio do Post a ser atualizado no site primeiro antes de enviar as notificações aos clientes. Ou seja, enviamos a mensagem ao WebSite Service (passo 3) depois de obter uma resposta indicando que a mensagem chegou com sucesso (passos 4a e 4b) e em seguida enviamos a mensagem de notificação aos clientes (passo 5). Isso evita o transtorno tratado anteriormente e também deixa o sistema mais resiliente e rastreável a falhas!\nPor fim, segue o fluxo refatorado:\nIsso é tudo pessoal… #Nesse artigo vimos detalhes do Saga que não são encontrados tão facilmente juntamente com problemas que aparecem ao implementá-lo. Esse padrão é comumente utilizado com diversos padrões como CQRS e Event-Driven, agregando a arquitetura do sistema e fortificando sua estrutura em diversas aplicações e produtos.\nEspero que tenha aumentado mais o seu conhecimento e leque de técnicas aplicadas a engenharia e arquitetura de software! Até a próxima o/\nReferências e Links úteis # Exemplo de Uso da Saga com Kafka e .Net: https://github.com/luanmds/kafka-dotnet-study/tree/main/sample-02 https://learn.microsoft.com/en-us/azure/architecture/reference-architectures/saga/saga https://medium.com/codex/compensating-transaction-in-microservices-15b1f88a7c29 https://livebook.manning.com/book/microservices-patterns/chapter-4/ https://blog.sofwancoder.com/try-confirm-cancel-tcc-protocol https://en.wikipedia.org/wiki/Inter-process_communication https://en.wikipedia.org/wiki/Two-phase_commit_protocol https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf https://en.wikipedia.org/wiki/Long-lived_transaction https://www.lifewire.com/the-acid-model-1019731 ","date":"13 fev. 2025","permalink":"https://luanmds.com/posts/saga-pattern-resumo/","section":"Posts","summary":"Guia compacto sobre o Saga Pattern — como gerenciar transações distribuídas em microserviços com Choreography e Orchestration.","title":"Saga Pattern - Um resumo com Caso de Uso"},{"content":"","date":null,"permalink":"https://luanmds.com/categories/system-design/","section":"Categories","summary":"","title":"System-Design"},{"content":"Use o botao de busca no topo (lupa) ou pressione / para abrir a busca rapida.\n","date":null,"permalink":"https://luanmds.com/search/","section":"Luan Mello","summary":"","title":"Buscar"},{"content":" Sou Engenheiro de Software com foco em Sistemas Distribuídos e Arquitetura de Backend. Com passagens por empresas como Stone, Saphira e BTG Pactual, especializei-me na construção de microsserviços de alta escala e fluxos de dados resilientes utilizando tecnologias como .NET, Kafka, Azure/AWS, entre outras.\nTech Stack \u0026amp; Expertise # .NET \u0026amp; C#: Desenvolvimento de serviços de missão crítica. Arquitetura: Event-Driven, CQRS, Event Sourcing e Modelagem C4/UML. Infraestrutura: Cloud (Azure/AWS), Docker e mensageria com Kafka. Qualidade: Clean Code, testes automatizados e práticas de engenharia moderna. Outras tecnologias: Python, JavaScript/TypeScript\u0026hellip; Escrevo para documentar meus estudos compartilhando conhecimentos aplicados no dia a dia e ajudando outras pessoas a tomar melhores decisões de engenharia.\nConecte-se comigo # ","date":null,"permalink":"https://luanmds.com/about/","section":"Luan Mello","summary":"","title":"Sobre"}]