Background
I am developing a number of business applications within a community. Many of these apps rely on callouts to another ERP system to verify or retrieve or push data. In order to adequately test the code in these apps, we need to be able run through unit tests as community users. I am using the mock response interface to create simulated API responses in tests.
Problem
Unfortunately, there does not seem to be an easy way to create a community user, use System.runAs(myuser) to then exercise my callout code. The insertion of a user, even with a properly placed Test.starTest() method always throws the error:
System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out
While querying for a user would work, it creates new problems because Community and Portal users only exist in full sandboxes. They are not copied to developer sandboxes because portal users have tie-ins to data (Contact and Account), which does not exist in a developer sandbox copy, and neither do the users. The other issue is that I really need to create certain scenarios for testing, and having to query for them is much harder.
What I've tried
Here is a super-simple reproduction of the error. First 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;
}
}
And the test class with a number of 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 seems like with a properly placed Test.startTest() method that you should be able to create a user, runAs(thatuser) and do a mock callout. Does anyone have any other ideas for a work-around?