Por que seu código C# está trabalhando em vão (e como corrigir isso)
\n\nVocê passou semanas refatorando o algoritmo de recomendações. A nova versão do RecommendationService parece impecável, o código mais limpo, as consultas LINQ mais rápidas. Você implanta a atualização em produção, e… a conversão cai 15%. Familiar? Se não — você teve sorte. Se sim — você acabou de perder dinheiro e tempo, porque confiou na intuição, e não nos dados.
No mundo do desenvolvimento C#, existe uma regra de ouro: “Nunca confie na sua opinião, confie na estatística”. É para isso que existe o teste A/B (ou teste de divisão). Isso não é um truque de marketing — é uma abordagem de engenharia para a tomada de decisões. Neste artigo, vamos abordar como implementar um teste A/B completo na sua aplicação C#: desde a arquitetura até o cálculo da significância estatística. E sim, terá muito código.
\n\nO que é teste A/B no contexto do C#?
\n\nImagine que você tem duas variantes da mesma lógica — o grupo de controle A (versão antiga) e o grupo experimental B (versão nova). Você distribui os usuários aleatoriamente entre eles, coleta métricas e, com a ajuda da matemática, determina qual variante é realmente melhor.
\n\nEm C#, isso pode envolver qualquer coisa:
\n- \n
- Alteração do algoritmo de busca \n
- Novo design do formulário de pagamento \n
- Diferentes opções de ordenação de produtos \n
- Experimentos com tempo de cache \n
- Teste de modelos de ML \n
Ponto-chave: teste A/B em C# não é sobre “colocar uma flag no config”. É sobre arquitetura limpa, randomização e coleta de métricas sem bugs.
\n\nArquitetura de teste A/B: Layer Cake em C#
\n\nVamos direto à prática. Vou mostrar uma arquitetura mínima, mas funcional. Ela consiste em três camadas:
\n\n- \n
- Assigner — decide em qual grupo o usuário caiu. \n
- Feature Decision — qual variante de código executar. \n
- Tracker — registra o evento e o resultado. \n
Passo 1: Assigner — quem é você, usuário?
\n\nO bug mais comum é a randomização incorreta. Usar Random é uma má ideia, porque ao reiniciar a aplicação, os grupos “flutuam”. A solução correta é a distribuição determinística baseada no UserId.
public interface IExperimentAssigner\n{\n string GetVariant(string userId, string experimentName);\n}\n\npublic class ConsistentExperimentAssigner : IExperimentAssigner\n{\n public string GetVariant(string userId, string experimentName)\n {\n // Cria um hash estável a partir de userId + experimentName\n var hash = HashCode.Combine(userId, experimentName);\n // Pega o módulo de 100 — obtém a porcentagem\n var bucket = Math.Abs(hash) % 100;\n \n // Supondo 50% em A, 50% em B\n return bucket < 50 ? "A" : "B";\n }\n}\n\n\nAgora, o mesmo usuário estará sempre no mesmo grupo para um experimento específico. Isso é crítico para a correção dos dados.
\n\nPasso 2: Feature Decision — a batalha dos algoritmos
\n\nAgora, precisamos que o código escolha qual lógica executar. Usamos o padrão Strategy com uma factory.
\n\npublic interface IPricingStrategy\n{\n decimal CalculatePrice(decimal basePrice);\n}\n\npublic class OldPricingStrategy : IPricingStrategy\n{\n public decimal CalculatePrice(decimal basePrice) \n => basePrice; // Sem desconto\n}\n\npublic class NewPricingStrategy : IPricingStrategy\n{\n public decimal CalculatePrice(decimal basePrice) \n => basePrice * 0.9m; // Desconto de 10%\n}\n\npublic class PricingStrategyFactory\n{\n private readonly IExperimentAssigner _assigner;\n\n public PricingStrategyFactory(IExperimentAssigner assi