0

Hey I have created a custom apex controller for us to use in cases where we need to lookup IDs from salesforce based on our own IDs coming from our own system. Now I want to test this controller class but I have never expected that it would be so hard to test this.

@RestResource(urlMapping='/accounts/*')
global class CustomAccountController {

    @HttpGet
    global static void getAccountByCustomerId() {
        RestRequest request = RestContext.request;
        String customerId = request.requestURI.substring(request.requestURI.lastIndexOf('/')+1);
    
        Account[] accountId = [SELECT id FROM Account WHERE Customer_Id__c = :customerId];
        Opportunity[] opportunityId = [SELECT id FROM Opportunity WHERE Customer_Id__c = :customerId];
        Lead[] leadId = [SELECT id FROM Lead WHERE Customer_Id__c = :customerId];
    
        SalesforceIds ids = new SalesforceIds();
    
        if (accountId.size() > 0) {
            ids.setAccountId(accountId[0].Id);
        }
    
        if (opportunityId.size() > 0) {
            ids.setOpportunityId(opportunityId[0].Id);
        }
    
        if (leadId.size() > 0) {
            ids.setLeadId(leadId[0].Id);
        }
    
        RestContext.response.responseBody = Blob.valueOf(JSON.serialize(ids));
    }
}

then I have my Test class as such:

@isTest
private class CustomAccountControllerTest {          

    @isTest
    static void testReturnIds(){
        initData();

        Test.startTest();
            RestRequest request = new RestRequest();
            RestResponse response = new RestResponse();

            request.requestURI = '/services/apexrest/accounts/123';
            request.httpMethod = 'GET';
            RestContext.request = request;
            RestContext.response = response;

            // Test.createStub(Account, init());

            Test.setMock(HttpCalloutMock.class, new MockSalesforceIdsResponse());

            CustomAccountController.getAccountByCustomerId();

            String res = response.responseBody.toString();

            System.assertEquals(getBody(), res);

        Test.stopTest();
        
    }

    // @IsTest
    // static void testNoIdsFound(){
        
    // }

    private static String getBody() {
        return '{"opportunityId":"0061j00000N0tooAAB","leadId":"00Q1j000007HrxfEAC","accountId":"0011j00001qtE4mAAE"}';
    }

    static void initData() {
        // Accounts
        Account account = new Account();
        account.Name = 'My Test Account';
        account.Customer_Id__c = '123';

        insert account;

        // Leads
        Lead lead = new Lead();
        lead.Name = 'My Test Lead';
        lead.Customer_Id__c = '123';

        insert lead;

        // Opportunities
        Opportunity opportunity = new Opportunity();
        opportunity.Name = 'My Test Opportunity';
        opportunity.Customer_Id__c = '123';

        insert opportunity;
    }
}

Now the current issue is that I get a validation rule not allowing to create new SObjects with the same Customer_Id__c is it possible to disable that rule in the test? As well another problem is that I am not able to set specific SObject Ids that I want for my testing case just getting the error that the ID field is Not Writable on ex. Account. I have tried all kinds of stuff even tried creating a Lead and trying to Convert that lead just ending up getting a FIELD_INTEGRITY_EXCEPTION. The Customer_Id__c is possible to exist in all three SObjects but Opportunity just copies it from the associated Account and Account gets it from Lead when Lead is converted. The Test.setMock(... I can even remove because it does not seem to do anything anyways just something I tried from some of the Apex sources from the documentation. Anyway I am running out of options on how to even write unit tests in Apex for a simple class as the CustomAccountController, and yes name might be misleading in regards that I am fetching something else but that I will fix once I have this test under control.

1
  • 1
    1. You can simply insert SObjects into the database as part of your test initialization. 2. I see no need for the callout mocking since you are testing your own REST API service. 3. Simply call the getAccountByCustomerId method in your test and check the response. You will need to construct the fake request URL using the ID of an inserted test object.
    – Phil W
    Commented Jan 31, 2023 at 14:59

2 Answers 2

1

Expanding on what Phil W mentioned in the comments...

You're right that you can get rid of Test.setMock()

Test.setMock() is used when you're making a callout to something outside of Salesforce (or rather, outside of the org you're running the test in). If your code has Http callout = new Http(); and callout.send(<HttpRequest variable here>);, then you need to use Test.setMock() to prevent Salesforce from trying to make an actual callout in your test. Salesforce wouldn't send an actual callout in a test though, you'd just get an error saying "callouts aren't allowed in tests".

Since you don't have that in the code you're trying to test (the System Under Test, SUT), you don't need it.

It's not clear why you mention that Customer_Id__c can't be re-used, but...

Validation rules can't be "turned off" in a test unless you specifically created your validation rules to do that (by explicitly excluding certain profiles or custom permissions, or by using a Hierachy type Custom Setting).

e.g.

AND(
    /* 
      This would mean that people with the "MyPermission" custom permission would
        be exempt from this rule
    */
    NOT($Permission.MyPermission),
    <some condition here>
)

Unless you can only set Customer_Id__c through a lead conversion

Then I would just not worry about that part.

If you can just set that field on your Account and Opportunity, then do that.

If the field on your Opportunity is a formula field, then you'll need to set AccountId with the Id of the test Account that you insert before the Opp.

On setting record Ids in tests

I believe there is a way to do this, but it's not appropriate in this case. Hard-coding Ids is almost universally bad practice. Your getBody() method needs to change.

Best practice here is to have an @TestSetup annotated method (static void) that does things like creating and inserting SObjects. Salesforce only runs the @TestSetup method once (at least they're supposed to. I'm not very convinced about that fact given the governor limits used in an @TestSetup method are counted against every test method), so it helps make your tests faster.

The downside to that approach is that you can't store the resulting Ids in a way that allows you to access them inside each test method (static variables are cleared out between the end of the @TestSetup method and each @isTest method). You need to query for that data.

You could change getBody() to query for your 3 SObjects (though SOSL is probably a better choice here), but having a test that basically repeats the code you're trying to test is a red flag. Perhaps the approach to take here is to not assert that you get the exact Ids of the test records, but rather to assert whether you simply get any Id back for a particular SObject.

ex.

// Data inserted here will be available in all tests
@TestSetup
static void initData(){
   // existing code omitted
}

@isTest
static void myTest(){
    // What you have is the correct way to set up request data to be available
    //   to your SUT
    RestRequest request = new RestRequest();
    RestResponse response = new RestResponse();

    request.requestURI = '/services/apexrest/accounts/123';
    request.httpMethod = 'GET';
    RestContext.request = request;
    RestContext.response = response;

    // I generally prefer to only wrap the execution of the SUT itself in
    //   start/stop test.
    // This helps give you a clear picture of the limits usage for just the thing
    //   you're testing
    Test.startTest();

    // Calling your method directly is precisely what you want to do
    CustomAccountController.getAccountByCustomerId();

    Test.stopTest();
    
    String res = response.responseBody.toString();

    // Hard to say exactly what your assertion should be since we don't know what
    //   your SalesforceIds class looks like
    // For a plain true/false result, System.assert() or the newer System.Assert.isTrue()
    //   is appropriate
    // While the 1 argument version would work, it's good practice to include a specific
    //   message for when the assertion fails (helps you identify the issue and where
    //   the failure was)
    System.assert(res.contains('001'), 'Did not return an expected Account Id');
    //System.Assert.isTrue(res.contains('001'), 'Did not return an expected Account Id');

}
0

Thanks for a quick answer @DerekF

I don't agree that hard coding Ids is universal bad practice when testing. It's very common in a lot of programming languages that when unit testing you want to setup your Data like such:

void testAccountReturned() {
  Account a = db.getAccountById('12345');

  assertEquals('12345', a.getId());
}

void createTestData() {
  Account newAccount = new Account();
  newAccount.setId('12345');
  newAccount.setName('Test Account');

  save(newAccount);
} 

and you want to be able to test that you can actually lookup your data by the specific ID.

Anyways, nevertheless your answer was very helpful and thanks for that. I just ended up with the following solution:

@isTest
private class CustomAccountControllerTest {     

    @isTest
    static void testReturnIds(){
        accountSetup();

        RestRequest request = new RestRequest();
        RestResponse response = new RestResponse();

        request.requestURI = '/services/apexrest/accounts/123';
        request.httpMethod = 'GET';
        RestContext.request = request;
        RestContext.response = response;

        Test.startTest();

            CustomAccountController.getAccountCustomerId();

        Test.stopTest();

        String res = response.responseBody.toString();

        System.assert(res.contains('001'), 'No Account found');
        
    }

    static void accountSetup() {
        // Accounts
        Account account = new Account();
        account.Name = 'My Test Account';
        account.Customer_Id__c = '123';

        insert account;
    }
}

I would like to upvote/accept your answer but for some reason I cannot since I am missing reputation. Accepting it might be because I posted as guest and first created my self as a user afterwards, sorry for that.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .