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 = true, totalAmount = 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 2N.
| Número de Condições na Regra (N) | Combinações Exaustivas (2N) | Cenários Obrigatórios em MC/DC (N+1) |
|---|---|---|
| 2 variáveis | 4 testes | 3 testes |
| 3 variáveis | 8 testes | 4 testes |
| 4 variáveis | 16 testes | 5 testes |
| 6 variáveis | 64 testes | 7 testes |
| 10 variáveis | 1.024 testes | 11 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 é
truee o resultado final da decisão éX. - Um teste onde essa mesma condição passa para
falsee 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 2N 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>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=(A∧B)∨C
Tabela-verdade completa (8 combinações)
| Caso | Condição A (Valor > 50k) | Condição B (ClienteGold) | Condição C (OverrideDiretor) | (A∧B) | Decisão Final | Papel no MC/DC |
|---|---|---|---|---|---|---|
| 1 | True | True | False | True | True | Base para pares de A e B |
| 2 | False | True | False | False | False | Par de A (com Caso 1) e Base de C |
| 3 | True | False | False | False | False | Par de B (com Caso 1) |
| 4 | False | False | False | False | False | Descartável (redundante) |
| 5 | True | True | True | True | True | Descartável (curto-circuito) |
| 6 | False | True | True | False | True | Par de C (com Caso 2) |
| 7 | True | False | True | False | True | Descartável (redundante) |
| 8 | False | False | True | False | True | Descartá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,F→True) com o Caso 2 (F,T,F→False). Repare: tanto B (
True) quanto C (False) mantiveram-se inalterados. Bastou alternar A de verdadeiro para falso e a aprovação caiu. Isso comprova formalmente o peso de A. - Independência da Condição B: Comparamos o Caso 1 (T,T,F→True) com o Caso 3 (T,F,F→False). Aqui, A (
True) e C (False) continuam exatamente como estavam. Mudar apenas B deTrueparaFalsefoi suficiente para rejeitar a solicitação. - Independência da Condição C: Comparamos o Caso 2 (F,T,F→False) com o Caso 6 (F,T,T→True). O termo conjunto (A∧B) 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.
