html code

Difficulty in unit testing, simulating callouts, and execution under a specific user context

Background

I’m building multiple business applications within a community, and these apps heavily depend on making callouts to an ERP system for data verification, retrieval, or pushing data. To properly test the code in these applications, it’s crucial to run unit tests as community users. I’m utilizing the mock response interface to simulate API responses within the tests.

Problem

Regrettably, it appears challenging to easily generate a community user and utilize System.runAs(myuser) to test my callout code. Despite attempting to insert a user, even with a correctly positioned Test.startTest() method, it consistently triggers an error:

System.CalloutException: There are pending, uncommitted actions. Please commit or rollback these actions before making the callout.

Although querying for a user might seem feasible, it introduces new challenges. Community and Portal users are exclusively available in full sandboxes, not in developer sandboxes due to their dependencies on Contact and Account data, which aren’t part of a developer sandbox copy. Additionally, generating specific testing scenarios becomes more complicated when reliant on querying for these users.

What I’ve tried

Below is a straightforward replication of the error. Initially, the callout class:

public with sharing class TestCalloutRunAs {
    public static HttpResponse getInfoFromExternalService() {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('http://api.salesforce.com/foo/bar');
        req.setMethod('GET');
        Http h = new Http();
        HttpResponse res = h.send(req);
        return res;
    }
}

The following is the test class featuring several commented scenarios:

global with sharing class testCalloutRunAs_TEST {

// THIS WORKS - running in system mode
@isTest static void testCallout() {

    // Set mock callout class 
    Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator());

    // Call method to test.
    // This causes a fake response to be sent
    // from the class that implements HttpCalloutMock. 
    HttpResponse res = TestCalloutRunAs.getInfoFromExternalService();

    // Verify response received contains fake values
    String contentType = res.getHeader('Content-Type');
    System.assert(contentType == 'application/json');
    String actualValue = res.getBody();
    String expectedValue = '{"foo":"bar"}';
    System.assertEquals(actualValue, expectedValue);
    System.assertEquals(200, res.getStatusCode());
}

// THIS WORKS - because we query for an existing user AND do startTest
@isTest static void testCalloutWithRunAs() {

    account a = new account(name = 'test acct');
    insert a;
    contact c = new contact(lastname = 'test', accountid = a.id);
    insert c;
    user u = [select id from user where profile.name = 'System Administrator' and isActive = true limit 1];
    u.contact = c;

    system.runAs(u) {

        Test.startTest();

        // Set mock callout class 
        Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator());

        // Call method to test.
        // This causes a fake response to be sent
        // from the class that implements HttpCalloutMock. 
        HttpResponse res = TestCalloutRunAs.getInfoFromExternalService();

        // Verify response received contains fake values
        String contentType = res.getHeader('Content-Type');
        System.assert(contentType == 'application/json');
        String actualValue = res.getBody();
        String expectedValue = '{"foo":"bar"}';
        System.assertEquals(actualValue, expectedValue);
        System.assertEquals(200, res.getStatusCode());

    }

}

// THIS FAILS - because we try to run as a user we create
@isTest static void testCalloutWithRunAsCreatedUser() {

    account a = new account(name = 'test acct');
    insert a;
    contact c = new contact(lastname = 'test', accountid = a.id);
    insert c;
    profile p = [select id from profile limit 1];
    user u = new User(alias = 'person', email='guest@testpkg.com',
                     emailencodingkey='UTF-8', firstname='Test', lastname='Person', languagelocalekey='en_US',
                     localesidkey='en_US', profileid = p.id,
                     timezonesidkey='America/Los_Angeles', username='guest@testpkg.com');
    insert u;

    system.runAs(u) {

        Test.startTest();

        // Set mock callout class 
        Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator());

        // Call method to test.
        // This causes a fake response to be sent
        // from the class that implements HttpCalloutMock. 
        HttpResponse res = TestCalloutRunAs.getInfoFromExternalService();

        // Verify response received contains fake values
        String contentType = res.getHeader('Content-Type');
        System.assert(contentType == 'application/json');
        String actualValue = res.getBody();
        String expectedValue = '{"foo":"bar"}';
        System.assertEquals(actualValue, expectedValue);
        System.assertEquals(200, res.getStatusCode());

    }

}

global class MockHttpResponseGenerator implements HttpCalloutMock {

    // Implement this interface method
    global HTTPResponse respond(HTTPRequest req) {
        // Optionally, only send a mock response for a specific endpoint
        // and method.
        System.assertEquals('http://api.salesforce.com/foo/bar', req.getEndpoint());
        System.assertEquals('GET', req.getMethod());

        // Create a fake response
        HttpResponse res = new HttpResponse();
        res.setHeader('Content-Type', 'application/json');
        res.setBody('{"foo":"bar"}');
        res.setStatusCode(200);
        return res;
    }
}
}

It appears that by appropriately implementing the Test.startTest() method, you should be able to create a user, use runAs(thatuser), and perform a mock callout. Does anyone have alternative suggestions for a workaround?

3 Answers

Having recently gone through this exercise, I’m not certain that you’re properly creating the communities portal user. From what you posted of the code above, you first need an owner for the account.

// THIS FAILS - because we try to run as a user we create
 @isTest static void testCalloutWithRunAsCreatedUser() {

Are you already using RunAs at this point? If so, did you specify a UserRoleId and ProfileId for the User? The Portal User essentially needs the UserRoleId of the User that creates him/her since a Communities Portal User has no Role. The Account and the Contact are both owned by the User who creates them, thus the need for the RunAs which it appears as though you already know.

account a = new account(name = 'test acct');
 insert a;
 contact c = new contact(lastname = 'test', accountid = a.id);
 insert c;
 profile p = [select id from profile limit 1];
 user u = new User(alias = 'person', email='guest@testpkg.com',
                 emailencodingkey='UTF-8', firstname='Test', lastname='Person', languagelocalekey='en_US',
                 localesidkey='en_US', profileid = p.id,
                 timezonesidkey='America/Los_Angeles', username='guest@testpkg.com');
insert u;

The explanation I just gave is why the above would fail if User u is the RunAs User. If they’re the Communities Portal User, the ProfileId for the Communities Guest User needs to have that particular license associated with it, not just any ProfileId.

One caveat is that any org can change the default ProfileName related to a License by cloning it when they create the Profile they intend to actually use, but for your purposes of a developer’s test class, using the default name for a Communities Portal Profile/license in your query should be no problem and it could very well resolve your problem.

So you want to mix dml inserts and make callouts in your tests? Thats Cray Cray!

Here’s the low down on how to get around the “You have uncommitted changes pending please commit or rollback…” when trying to mix DML and HTTPCallouts in your test methods.

First, a little background and a health and safety warning. Sooner or later you’ll be faced with testing a method that both a: manipulates existing data, and b: calls out to a third party service for more information via HTTP. Sadly, this is one of those situations where testing the solution is harder than the actual solution. In a testing situation, you should be inserting your data that your method is going to rely on. But this making a DML call — insert — will prevent any further http callouts from executing within that Apex context. Yuck. That means inserting say, an account, and then making a call out with some of that data … well that just won’t work. No Callouts after a DML call.

So lets cheat a bit. Apex gives us two tools that are helpful here. The first is the @future annotation. Using the @future annotation and methodology allows you to essentially switch apex contexts, at the cost of synchronous code execution. Because of the Apex context switch governor limits, DML flags are reset. Our second tool is a two-fer of Test.startTest() and Test.stopTest(). (you are using Test.startTest() and Test.StopTest() right?) Among their many tricks is this gem: When you call Test.stopTest(); all @future methods are immediately executed. When combined together these two tricks give us a way to both insert new data as part of our test, then make callouts (which we’re mocking of course) to test, for example, that our callout code is properly generating payload information etc. Here’s an example:

/In a class far far away...
@future
global static void RunMockCalloutForTest(String accountId){
     TestRestClient trc = new TestRestClient();
     id aId;
     try {
          aId = (Id) accountId;
     } catch (Exception e) {
          throw new exception('Failed to cast given accountId into an actual id, Send me a valid id or else.');
     }
     Account a = [select id, name, stuff, foo, bar from Account where id = :aId];

     //make your callout
     RestClientHTTPMocks fakeResponse = new RestClientHTTPMocks(200, 'Success', 'Success',  new Map<String,String>());
     System.AssertNotEquals(fakeResponse, null);
     Test.setMock(HttpCalloutMock.class, fakeResponse);
     System.AssertNotEquals(trc, null); //this is a lame assertion. I'm sure you can come up with something useful!
     String result = trc.get('http://www.google.com');

}

//In your test…
@isTest
static void test_method_one() {

     //If you're not using SmartFactory, you're doing it way too hard. (and wrong)
     Account account = (Account)SmartFactory.createSObject('Account');
     insert account;
     Test.startTest();
     MyFarawayClass.RunMockCalloutForTest(account.id);
     Test.StopTest();
}

This works, because we can both a: switch to an Apex context that’s not blocked from making HTTP Callouts, and b: force that asynchronous Apex context to execute at a given time with test.stopTest().

You don’t need to insert the user for the runAs.

However if you really need to insert a user for your test, do it inside the runAs