Todo desenvolvedor Salesforce sabe da importância das classes de teste, elas além de testarem se o que desenvolvemos está funcionando como esperado, precisamos também garantir que as classes tenham pelo menos 75% de cobertura conforme requerido pela Salesforce para subir um código para produção.

O modelo de uma classe de teste costuma seguir uma receita muito simples, criar os dados para teste, chamar o método que queremos testar e verificar o retorno.

Porém, para cobrir classes que fazem queries de objetos externos precisamos adaptar esse modelo um pouco, mas antes vamos entender um pouco o que são os objetos externos.

O que são Objetos Externos

Objetos externos são parecidos com os objetos customizados, porém eles são armazenados fora da sua ORG. Eles podem ser dados armazenados em outros sistemas como ERPs, CRMs ou até mesmo uma outra ORG Salesforce, a chamada integração Salesforce to Salesforce.

Através da opção External Data Sources no Setup é possível configurar a fonte dos dados externos e fazer o mapeamento de tabelas para objetos e campos.

A nomenclatura dos objetos externos apresenta o sufixo __x, porém os campos seguem o padrão com o final __c.

Como usar os Objetos Externos

Embora os objetos externos não estejam armazenados na ORG ele funciona de maneira bem parecida com os objetos externos sendo apenas não possível fazer o compartilhamento de registros por Apex e também pela ausência de Triggers desses objetos.

Caso de uso

Para ilustrar um cenário trabalharemos com o objeto externo pedido (Orders__x). Esse objeto pode ser configurado seguindo esse projeto no Trailhead.

Imagine que temos a seguinte classe com dois métodos simples, um que busca os detalhes do pedido pelo Id e outro que trás todos os pedidos de um cliente.

public class SearchOrders {
    //procura pedido pelo Id
    public Orders__x searchOrdersById(Integer orderId) {
        List<Orders__x> ordersList = [SELECT Id, ExternalId, orderDate__c, orderID__c, shippedDate__c FROM Orders__x WHERE orderID__c =: orderId];
        return ordersList[0];
    }
    
    //procura pedidos pelo Id do cliente
    public List<Orders__x> searchOrdersByClientId(String clientId) {
         List<Orders__x> ordersList = [SELECT Id, ExternalId, orderDate__c, orderID__c, shippedDate__c FROM Orders__x WHERE customerID__c =: clientId];
         return ordersList;
    }
}

Se Orders__x fosse um objeto customizado nós poderíamos seguir a receita das classes de teste, criar registros, chamar os métodos e validar os retornos, porém no caso de objetos externos nós não podemos inserir registros ao executar os testes.

Então, o que fazer? Nesse caso, vamos primeiro criar uma classe Wrapper que será responsável por criar uma instância de pedido e que também conterá os métodos referentes a esse objeto. Veja como fica:

public class ExternalOrder {
    //variável para guardar dados de teste
    @TestVisible protected List<SObject> testData = new List<SObject>();

    //método usado em tempo de teste
    public void addTestData(List<SObject> records) {
        testData.addAll( records );
    }
    
    private static ExternalOrder order;
    private ExternalOrder(){}
    
    //crio uma única instância de ExternalOrder
    public static ExternalOrder getInstance() {
        if( order == null) {
            order = new ExternalOrder();
        }

        return order;
    }
    
    //procura pedido pelo Id
    public Orders__x searchOrdersById(Integer orderId) {
        List<Orders__x> ordersList = [SELECT Id, ExternalId, orderDate__c, orderID__c, shippedDate__c FROM Orders__x WHERE orderID__c =: orderId];
        
        //se teste rodando retorna os dados de teste
        //se não retorna os dados da query
        return (Test.isRunningTest()) ? (Orders__x) testData[0] : (ordersList.size() > 0) ? ordersList[0] : null;
    }
    
    //procura pedidos pelo Id do cliente
    public List<Orders__x> searchOrdersByClientId(String clientId) {
        List<Orders__x> ordersList = [SELECT Id, ExternalId, orderDate__c, orderID__c, shippedDate__c FROM Orders__x WHERE customerID__c =: clientId];

        //se teste rodando retorna os dados de teste
        //se não retorna os dados da query
        return (Test.isRunningTest()) ? (List<Orders__x>) testData : (ordersList.size() > 0) ? ordersList : null;
    }
}

Repare que é uma solução simples, sempre que o teste estiver rodando retornamos a variável testData, caso contrário o fluxo segue normal retornando o resultado da query.

Agora que colocamos as queries na classe ExternalOrder precisamos mudar os métodos na classe SearchOrders. Veja como fica:

public class SearchOrders {

    //procura pedido pelo Id
    public Orders__x searchOrdersById( Integer orderId) {
         Orders__x order = ExternalOrder.getInstance().searchOrdersById( orderId );
         return order;
    }
    
    //procura pedidos pelo Id do cliente
    public List<Orders__x> searchOrdersByClientId( String clientId) {
        List<Orders__x> ordersList = ExternalOrder.getInstance().searchOrdersByClientId( clientId );
        return ordersList;
    }
}

Agora vamos para a classe de teste.

@isTest
public class SearchOrdersTest {
    
    //cria dados para teste
    private static void createExternalOrders() {
        Orders__x order1 = new Orders__x (
            CustomerID__c = '123', 
            ExternalId = '001', 
            orderDate__c = Date.today().addDays(-5),
            OrderID__c  = 123456, 
            shippedDate__c = Date.today().addDays(-3)
        );

        Orders__x order2 = new Orders__x(
            CustomerID__c = '123', 
            ExternalId = '002', 
            orderDate__c = Date.today().addDays(-4), 
            OrderID__c  = 123457, shippedDate__c = Date.today().addDays(-2)
        );

        List<Orders__x> orderList = new List<Orders__x>{order1, order2};
        
        //insere dados de teste na variável testData de ExternalOrder
        ExternalOrder.getInstance().addTestData(orderList);
    }
	
    @isTest
    static void searchOrdersByIdTest() {
        createExternalOrders();

        test.startTest();
        SearchOrders search = new SearchOrders();
        Orders__x order = search.searchOrdersById(123456);
        System.assertEquals(123456, order.OrderID__c);
        test.stopTest();
    }
    
    @isTest
    static void searchOrdersByClientIdTest() {
        createExternalOrders();

        test.startTest();
        SearchOrders search = new SearchOrders();
        List<Orders__x> ordersList = search.searchOrdersByClientId('123');
        System.assertEquals(2, ordersList.size());
        test.stopTest();
    }
}

Repare que criamos um método chamado createExternalOrders, ele cria os dados para serem utilizados no contexto de teste e os armazena na variável testData da classe ExternalOrder. Este método não pode ter a anotação testSetup.

Dentro do método dos testes o que fazemos é chamar o método createExternalOrders, chamar o método que queremos cobrir e validar o retorno, embora aqui a validação não seja necessária.

Pronto, agora podemos rodar os testes e verificar a cobertura.

Resultado - Cobertura de Classes - Objetos Externos

Classes ExternalOrder e SearchOrders com 100% de cobertura.

Classes cobertas com sucesso!

Infelizmente nesse cenário nós estamos apenas cobrindo as linhas e não realmente testando o retorno dos métodos, mas acredito ser o único jeito de ser feito quando trabalhando com objetos externos.

 

Diogo Ramos

Diogo Ramos

Salesforce Developer at Avanxo

Formado em Sistemas da Informação pela Anhembi Morumbi, iniciei minha carreira de desenvolvedor com Java e por conta da semelhança do Java com o Apex acabei caindo no mundo Salesforce em 2017 e cá estou desde então.

Posso dizer que me apaixonei rapidamente pela plataforma e por tudo que é possível fazer com ela. Muitos dizem que sou um viciado em Trailhead, pois tenho mais de 400 badges, além de 12 certificações, já eu digo que gosto de aprender algo novo todo dia.

Atualmente trabalho em consultoria atendendo grandes clientes, atuo fazendo tanto a parte de administração e customização como desenvolvimento.

Acompanhe meu Trailhead aqui.