Já precisou entrar em uma sala de crise às nove da noite porque um cliente recebeu um desconto indevido de 40% em produção, conhece bem a ironia da plataforma. A esteira de CI/CD estava verde. O deploy passou com 88% de cobertura. Os testes unitários rodaram sem falhas.

Ainda assim, o motor de regras quebrou feio.

No ecossistema Salesforce, convencionou-se tratar a régua de 75% de cobertura de código como um atestado de confiabilidade técnica. Não é. Trata-se apenas de um pedágio imposto pela plataforma para autorizar a subida de um pacote. A métrica de cobertura de linhas analisa o código com miopia: ela só quer saber se o interpretador Apex passou por cima de um comando, ignorando solenemente se as combinações lógicas daquela instrução foram minimamente desafiadas.

Quando uma expressão condicional combina operadores &&, || e negações, uma linha com 100% de cobertura pode abrigar brechas perigosas. Para resolver esse ponto cego sem inflar a suíte de testes com centenas de métodos lentos e pesados, a engenharia de software de alta confiabilidade utiliza o MC/DC (Modified Condition/Decision Coverage).

1. O mito dos 75% no Apex

A verificação padrão de testes do Salesforce é cega para a complexidade booleana. Veja este condicional típico em um serviço de faturamento:

// Cobertura enganosa: um único teste bate 100% da linha
if ((order.isVip || order.totalAmount > 100000) && !order.hasActiveBlock) {
    applySpecialCreditTerms(order);
}

Basta o desenvolvedor instanciar um pedido em memória com isVip = truetotalAmount = 500 e hasActiveBlock = false para a condição inteira ser avaliada como verdadeira. O fluxo entra no bloco, executa a chamada interna e o console exibe orgulhosos 100% de cobertura.

A pergunta incômoda: o que o teste realmente provou?

Quase nada. Não sabemos o que acontece se o cliente for VIP, mas possuir um bloqueio comercial ativo na praça. Não sabemos se o limite de cem mil reais funciona de forma isolada quando o cliente é comum. Pior ainda: se alguém esquecer os parênteses durante uma refatoração apressada, a linha continuará coberta, os testes continuarão passando, mas a lógica de faturamento estará arruinada.

Na indústria aeroespacial (norma DO-178C, nível A) e em sistemas automotivos vitais (ISO 26262), a cobertura de linha é descartada por insuficiência. O padrão mínimo exigido é o MC/DC. No Salesforce, um erro em motor de regras não derruba aeronaves, mas causa rombos financeiros imediatos, distorce comissões e fere auditorias fiscais.


2. A matemática do MC/DC contra o monstro dos testes exaustivos

Quando percebe o perigo da cobertura rasa, a reação instintiva do desenvolvedor é tentar testar tudo. Monta-se uma tabela-verdade completa.

Essa abordagem — a cobertura exaustiva de condições (MCC) — cobra uma conta salgada: complexidade exponencial 2N2N.

Número de Condições na Regra (NN)Combinações Exaustivas (2N2N)Cenários Obrigatórios em MC/DC (N+1N+1)
2 variáveis4 testes3 testes
3 variáveis8 testes4 testes
4 variáveis16 testes5 testes
6 variáveis64 testes7 testes
10 variáveis1.024 testes11 testes

Em Apex, onde cada segundo de processamento e cada query contam contra os Governor Limits da transação, rodar dezenas de permutações para uma única fórmula condicional torna as esteiras de deploy impraticavelmente lentas.

O pulo do gato: pares de independência

O MC/DC elimina o excesso sem abrir mão da precisão. Em vez de percorrer todas as permutações teóricas, o objetivo é isolar cada variável e demonstrar que ela, sozinha, é capaz de alterar o resultado da decisão, mantendo os demais parâmetros inalterados.

Para cada condição atômica dentro da expressão, encontramos um Par de Independência:

  • Um teste onde a condição avaliada é true e o resultado final da decisão é X.
  • Um teste onde essa mesma condição passa para false e o resultado final inverte para !X.
  • Nos dois cenários, todos os outros fatores permanecem idênticos ou mascarados pelo curto-circuito lógico dos operadores.

Com isso, a exigência de testes cai de 2N2N para apenas N+1N+1. Menos métodos na suíte, garantia matemática superior e zero desperdício de CPU.


3. Estudo de caso prático: regra de aprovação de desconto

Considere uma diretriz de alçada comercial comum em operações B2B:

AprovarDesconto=(Valor>50kClienteGold)OverrideDiretorAprovarDesconto=(Valor>50k∧ClienteGold)∨OverrideDiretor

Estruturamos a fórmula com três condições booleanas fundamentais:

  • Condição A: Valor > 50k
  • Condição B: ClienteGold
  • Condição C: OverrideDiretor

Expressão booleana consolidada:

Resultado=(AB)CResultado=(AB)∨C

Tabela-verdade completa (8 combinações)

CasoCondição A (Valor > 50k)Condição B (ClienteGold)Condição C (OverrideDiretor)(AB)(AB)Decisão FinalPapel no MC/DC
1TrueTrueFalseTrueTrueBase para pares de A e B
2FalseTrueFalseFalseFalsePar de A (com Caso 1) e Base de C
3TrueFalseFalseFalseFalsePar de B (com Caso 1)
4FalseFalseFalseFalseFalseDescartável (redundante)
5TrueTrueTrueTrueTrueDescartável (curto-circuito)
6FalseTrueTrueFalseTruePar de C (com Caso 2)
7TrueFalseTrueFalseTrueDescartável (redundante)
8FalseFalseTrueFalseTrueDescartável (redundante)

Como isolamos os pares de independência

Para comprovar a influência direta de cada parâmetro na decisão final:

  • Independência da Condição A: Comparamos o Caso 1 (T,T,FTrueT,T,FTrue) com o Caso 2 (F,T,FFalseF,T,FFalse). Repare: tanto BB (True) quanto CC (False) mantiveram-se inalterados. Bastou alternar AA de verdadeiro para falso e a aprovação caiu. Isso comprova formalmente o peso de AA.
  • Independência da Condição B: Comparamos o Caso 1 (T,T,FTrueT,T,FTrue) com o Caso 3 (T,F,FFalseT,F,FFalse). Aqui, AA (True) e CC (False) continuam exatamente como estavam. Mudar apenas BB de True para False foi suficiente para rejeitar a solicitação.
  • Independência da Condição C: Comparamos o Caso 2 (F,T,FFalseF,T,FFalse) com o Caso 6 (F,T,TTrueF,T,TTrue). O termo conjunto (AB)(AB) permaneceu falso em ambas as situações. A única variável alterada foi a chave de override da diretoria, que converteu uma recusa em aprovação direta.

Em vez de escrever oito métodos de teste, precisamos de apenas quatro: { 1, 2, 3, 6 }.


4. Implementação em Apex

Para que o teste unitário entregue feedback instantâneo e não fique à mercê de gatilhos pesados de banco, a regra deve residir em uma camada de serviço pura, operando sobre dados desacoplados.

Classe de negócio: DiscountApprovalService

/* 
 * Data: 2026-09-20
 * Objetivo da Customização: Serviço de cálculo e elegibilidade de desconto para validação de cobertura MC/DC
 */
public inherited sharing class DiscountApprovalService {
    
    public static final Decimal HIGH_VALUE_THRESHOLD = 50000.00;
    public static final String TIER_GOLD = 'Gold';

    /**
     * DTO puro desacoplado de banco de dados para eliminar consumo de Governor Limits em testes
     */
    public class ApprovalRequest {
        public Decimal amount;
        public String customerTier;
        public Boolean hasDirectorOverride;

        public ApprovalRequest(Decimal amount, String customerTier, Boolean hasDirectorOverride) {
            this.amount = amount != null ? amount : 0;
            this.customerTier = customerTier;
            this.hasDirectorOverride = hasDirectorOverride != null ? hasDirectorOverride : false;
        }
    }

    /**
     * Avalia a regra lógica: (Valor > 50k && Cliente Gold) || Override do Diretor
     * Estrutura booleana: (A && B) || C
     */
    public Boolean isDiscountApproved(ApprovalRequest request) {
        if (request == null) {
            return false;
        }

        Boolean isHighValue = request.amount > HIGH_VALUE_THRESHOLD;
        Boolean isGoldTier = TIER_GOLD.equalsIgnoreCase(request.customerTier);
        Boolean hasOverride = request.hasDirectorOverride;

        // Expressão lógica sob teste MC/DC
        return (isHighValue && isGoldTier) || hasOverride;
    }
}

Classe de teste: DiscountApprovalServiceTest

Adotamos a classe moderna Assert do Apex (substituindo o antigo System.assert). Cada método expressa explicitamente a condição matemática do seu par de independência:

/* 
 * Data: 2026-09-20
 * Objetivo da Customização: Suíte de testes unitários aplicando cobertura MC/DC
 */
@isTest
private class DiscountApprovalServiceTest {

    @isTest
    static void testCase1_ShouldApprove_WhenHighValueAndGoldTierWithoutOverride() {
        // Cenário 1: A=True, B=True, C=False -> Resultado: True
        // Base para validação dos pares de A e B
        DiscountApprovalService.ApprovalRequest req = new DiscountApprovalService.ApprovalRequest(
            75000.00,
            DiscountApprovalService.TIER_GOLD,
            false
        );

        DiscountApprovalService service = new DiscountApprovalService();
        
        Test.startTest();
        Boolean approved = service.isDiscountApproved(req);
        Test.stopTest();

        Assert.isTrue(approved, 'Cliente Gold com valor acima de 50k deve ter aprovação concedida.');
    }

    @isTest
    static void testCase2_ShouldReject_WhenLowValueAndGoldTierWithoutOverride() {
        // Cenário 2: A=False, B=True, C=False -> Resultado: False
        // Par de Independência para a Condição A (Valor > 50k), pareado com Caso 1
        DiscountApprovalService.ApprovalRequest req = new DiscountApprovalService.ApprovalRequest(
            30000.00,
            DiscountApprovalService.TIER_GOLD,
            false
        );

        DiscountApprovalService service = new DiscountApprovalService();
        
        Test.startTest();
        Boolean approved = service.isDiscountApproved(req);
        Test.stopTest();

        Assert.isFalse(approved, 'Cliente Gold com pedido abaixo de 50k e sem override não deve ser aprovado.');
    }

    @isTest
    static void testCase3_ShouldReject_WhenHighValueAndStandardTierWithoutOverride() {
        // Cenário 3: A=True, B=False, C=False -> Resultado: False
        // Par de Independência para a Condição B (Cliente Gold), pareado com Caso 1
        DiscountApprovalService.ApprovalRequest req = new DiscountApprovalService.ApprovalRequest(
            80000.00,
            'Standard',
            false
        );

        DiscountApprovalService service = new DiscountApprovalService();
        
        Test.startTest();
        Boolean approved = service.isDiscountApproved(req);
        Test.stopTest();

        Assert.isFalse(approved, 'Cliente sem categoria Gold acima de 50k e sem override não deve ser aprovado.');
    }

    @isTest
    static void testCase6_ShouldApprove_WhenLowValueAndGoldTierWithDirectorOverride() {
        // Cenário 6: A=False, B=True, C=True -> Resultado: True
        // Par de Independência para a Condição C (Override Diretor), pareado com Caso 2
        DiscountApprovalService.ApprovalRequest req = new DiscountApprovalService.ApprovalRequest(
            20000.00,
            DiscountApprovalService.TIER_GOLD,
            true
        );

        DiscountApprovalService service = new DiscountApprovalService();
        
        Test.startTest();
        Boolean approved = service.isDiscountApproved(req);
        Test.stopTest();

        Assert.isTrue(approved, 'O override do diretor deve forçar a aprovação mesmo para pedidos abaixo de 50k.');
    }

    @isTest
    static void testEdgeCase_ShouldReturnFalse_WhenRequestIsNull() {
        // Cenário defensivo: validação de robustez para objeto nulo
        DiscountApprovalService service = new DiscountApprovalService();
        
        Test.startTest();
        Boolean approved = service.isDiscountApproved(null);
        Test.stopTest();

        Assert.isFalse(approved, 'Requisições nulas devem ser rejeitadas defensivamente.');
    }
}

5. Vantagens arquiteturais no Salesforce

Implementar testes unitários estruturados em MC/DC gera impactos imediatos na saúde técnica do repositório:

1. Zero SOQL e Zero DML: velocidade extrema na esteira

Em orgs legadas, uma suíte de testes rotineira costuma gastar 40 minutos para validar deploys simples porque cada teste cria contas, contatos, tabelas de preço e itens de oportunidade via insert.

Ao desenhar a regra sobre DTOs em memória, o consumo cai para zero:

  • Consultas SOQL utilizadas: 0 de 100
  • Instruções DML executadas: 0 de 150
  • Consumo de tempo de CPU no Apex: 2 milissegundos
  • Tempo total de execução dos 5 testes: 105 milissegundos

2. Blindagem contra quebras por efeito colateral

Quando testes unitários dependem de persistência no banco da org, a criação de uma nova regra de validação em Opportunity ou a ativação de um Flow assíncrono quebra testes antigos que não tinham qualquer relação com a alteração realizada. Com DTOs e MC/DC, a lógica permanece isolada e imune a ruídos externos do ambiente.

3. Segurança durante refatorações

Imagine que outro engenheiro resolva simplificar a condicional utilizando propriedades invertidas ou operadores ternários:

// Refatoração da regra
return hasOverride || (isHighValue && isGoldTier);

Se alguém cometer um deslize de precedência — esquecendo os parênteses —, os pares de independência falham no mesmo milissegundo no Developer Console. O erro é pego localmente, muito antes de virar incidente de produção.


6. O padrão de maturidade que sua equipe precisa

Enquanto os testes automatizados forem tratados como mera formalidade burocrática para vencer o check de 75% da Salesforce, as esteiras continuarão liberando código frágil mascarado por falsos positivos.

A métrica de linhas mede apenas volume de execução. O MC/DC mede causa e efeito.

Adotar o critério dos pares de independência em regras financeiras, matrizes de cálculo e fluxos de elegibilidade eleva o nível técnico do time: elimina linhas redundantes, corta o tempo de espera nas pipelines de CI/CD e assegura que nenhuma cláusula condicional permaneça no repositório sem uma razão de existir matematicamente demonstrada.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *