Unit testing Lightning Connect Apps

This month I had the pleasure of presenting at London’s Calling event an application I have done for learning how to use Lightning Connect to retrieve data from an external datasource, which in my case is Google Places API. If you want to know more about the application, here is the video of the presentation.

In the Q&A I had a question about Unit test, that at the moment I couldn’t shown. That question encouraged me to do some unit tests! So, here we have the results.

First of all, in the application we can differentiate two main areas to test: the code of the custom adapter and the code which uses the custom adapter (which could be tested in the same way I am going to explain if we were using an OData provider).

For testing the code of the custom adapter, we have to mock the Http callouts. Salesforce provides a way of doing this, providing to us the HttpCalloutMock interface, which we should implement with a test implementation. In my case, I have implemented it in a more or less dynamic way as follows:

@isTest
global class HttpCalloutMockImpl implements HttpCalloutMock
{
  private Map<String, String> responsesByEndpointURL = new Map<String, String>();

  public void addResponse(String endpointUrl, String response)
  {
    responsesByEndpointURL.put(endpointUrl, response);
  }

  global HTTPResponse respond(HTTPRequest req)
  {
    String endpointUrl = req.getEndpoint();
    String body = responsesByEndpointURL.get(endpointUrl);

    HttpResponse res = new HttpResponse();
    res.setHeader('Content-Type', 'application/json');
    res.setBody(body);
    res.setStatusCode(200);
    return res;
  }
}

 

Then, thanks to this, I am able to test any part of my custom adapter code that needs to perform a callout, as I can mock the callout response returning any json response for a specified endpoint URL being queried. For example, in this unit test I specify to my mock implementation that if certain endpoint URL is requested, the answer has to contain my favourite restaurant object information, then I am able to test the behaviour of whichever method that uses the json response.

@isTest
static void getRestaurantsByExternalIds_shouldReturnDeserializedRestaurant()
{
	// Given
	Restaurant myRestaurant = new Restaurant();
	myRestaurant.international_phone_number = '+34 555555555';
	myRestaurant.name = 'My Favourite Restaurant A';
	myRestaurant.place_id = 'A';
	myRestaurant.url = 'https://maps.google.com/?cid=10281119596374313554';
	myRestaurant.vicinity = 'My Favourite Street';
	myRestaurant.website = 'https://myfavouriterestaurant.com';

	GoogleResponse myResponse = new GoogleResponse();
	myResponse.result = myRestaurant;

	String endpointURL = GooglePlacesService.getRestaurantEndpoint(myRestaurant.place_id);
	String body = JSON.serialize(myResponse);

	HttpCalloutMockImpl calloutMockImpl = new HttpCalloutMockImpl();
	calloutMockImpl.addResponse(endpointURL, body);
	Test.setMock(HttpCalloutMock.class, calloutMockImpl);

	List<String> externalIds = new List<String>{myRestaurant.place_id};

	// When
	List<Restaurant> restaurants = GooglePlacesService.getRestaurantsByExternalIds(externalIds);

	// Then
	System.assertEquals(1, restaurants.size());
	System.assertEquals(myRestaurant.international_phone_number, restaurants[0].international_phone_number);
	System.assertEquals(myRestaurant.name, restaurants[0].name);
	System.assertEquals(myRestaurant.place_id, restaurants[0].place_id);
	System.assertEquals(myRestaurant.url, restaurants[0].url);
	System.assertEquals(myRestaurant.vicinity, restaurants[0].vicinity);
	System.assertEquals(myRestaurant.website, restaurants[0].website);
}

 

On the other hand, we have to test the code that uses our external objects and which is not part of the custom adapter implementation, and, as I said before, could be tested in the same way if we were using an OData adapter. For that case, mocking the Http callouts is not valid anymore, as Lightning Connect is not going to return you anything in test context for external objects queries. I have made some tests, and even the entry point of the custom adapter is never reached when executing a test that contains queries to a external object.

So, the only option that we have here is to mock the SOQL queries against the external object. For that I recommend you to use this open source Apex Mocks Framework . In my case, as I only wanted to do a simple demonstration, I have written the mocks myself.

For mocking my mapper class (which is the one which is going to perform the SOQL queries against my external object), I have defined a factory class that returns the real / test implementation depending on the context:

public with sharing class RestaurantMapperFactory
{
  public static IRestaurantMapper instance;

  public static IRestaurantMapper getInstance()
  {
    if (instance == null)
    {
      if (Test.isRunningTest())
	instance = (IRestaurantMapper) new RestaurantMapperMockImpl();
      else
	instance = (IRestaurantMapper) new RestaurantMapper();
    }
		
    return instance;
  }
}

 

Both, my mapper real implementation and my mapper test implementation, implement a common interface:

public interface IRestaurantMapper
{
  Restaurant__x getById(Id restaurantId);
  List<Restaurant__x> getByExternalIds(List<String> restaurantExternalIds);
  List<Restaurant__x> getByLocation(Decimal latitude, Decimal longitude);
}

 

Then, my test implementation is going to return whatever I want (notice that if you use Apex Mocks Framework, you will be able to specify the return values of the methods given certain conditions, which is much more flexible):

public with sharing class RestaurantMapperMockImpl implements IRestaurantMapper
{
  public Restaurant__x getById(Id restaurantId)
  {
     Restaurant__x restaurant = new Restaurant__x();
     restaurant.Name__c = 'My favourite restaurant';
     return restaurant;
  }

  public List<Restaurant__x> getByExternalIds(List<String> restaurantExternalIds)
  {
    List<Restaurant__x> restaurants = new List<Restaurant__x>();
    for (String restaurantExternalId : restaurantExternalIds)
    {
      Restaurant__x restaurant = new Restaurant__x();
      restaurant.ExternalId = restaurantExternalId;
      restaurant.Name__c = 'My favourite restaurant ' + restaurantExternalId;
      restaurants.add(restaurant);
    }

    return restaurants;
  }

  public List<Restaurant__x> getByLocation(Decimal latitude, Decimal longitude)
  {
    List<Restaurant__x> restaurants = new List<Restaurant__x>();
    for (Integer i=0; i<10; i++)
    {
      Restaurant__x restaurant = new Restaurant__x();
      restaurant.Name__c = 'My favourite restaurant ' + i;
      restaurants.add(restaurant);
    }
   
    return restaurants;
  }
}

 

I want to notice here that I have had to add creation capabilities to the custom adapter and configure the external datasource to allow it, because otherwise, I was not able of creating external objects as I am doing in the test implementation (failed at runtime).

Finally, I can unit test any method in my application that performs queries against my external objects thanks to this mock. For instance, in my case, I have tested this one:

@isTest
static void getByDistance_shouldReturnRestaurantListRetrievedFromMapper()
{
  // Given
  Decimal latitude = 1.23442354;
  Decimal longitude = -0.23236786;

  // When
  List<Restaurant__x> restaurants = RestaurantService.getByDistance(latitude, longitude);

  // Then
  System.assert(!restaurants.isEmpty());
}

 

Here is the complete code of the application. You can contact me if you have any doubts by twitter or email.

References:

https://developer.salesforce.com/forums/?id=906F00000005HkFIAU

http://salesforce.stackexchange.com/questions/97225/test-class-for-a-controller-that-has-logic-to-query-the-external-object-records

Salesforce Lightning Connect Custom Adapter Part 3 – Testing

One thought on “Unit testing Lightning Connect Apps

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s