+ Start a Discussion
Derek Davis 7Derek Davis 7 

Create a Test Class for Bitly Integration

Hello Everyone!

I was able to create a Bitly Integration using the awesome instructions provided by Doug Ayers shown here: https://douglascayers.wordpress.com/2015/10/21/salesforce-create-short-urls-with-bitly-process-builder-and-apex/

This is working well in the Sandbox, but I am not a new to APEX and attempting to create a Test Class so I can move to Production.

There are two different APEX Classes:
  • BitlyService
  • BitlyShortenURLInvocable
Could anyone give suggestions on how to create test classes? The full code is shown below. I understand that I need to create test data within the class, then use the test data to actually perform test, but I'm not understanding what that would look like in this senario. Any direction is appreciated. Thanks in advance!!

BitlyService Apex Class:
/**
 * Simple service to make http callout to
 * Bitly url shortener service.
 */
public class BitlyService {
    
    // reusable access token for oauth,
    // required when making API requests
    private String accessToken;
    
    public BitlyService() {
        this.accessToken = getAccessToken();
    }
    
    /**
     * Given a long URL will return the shortened version.
     * http://dev.bitly.com/links.html#v3_shorten
     */
    public String shorten( String url ) {
        
        HttpRequest req = new HttpRequest();
        req.setEndpoint(
            'callout:Bitly/v3/shorten' +
            '?access_token=' + this.accessToken +
            '&longUrl=' + EncodingUtil.urlEncode( url, 'UTF-8' ) +
            '&format=txt'
        );
        req.setMethod('GET');
        
        Http http = new Http();
        HttpResponse res = http.send(req);
        return res.getBody();
    }
    
    /**
     * Get the access token to make authenticated oauth calls.
     * The actual username/password credentials are stored in
     * Named Credentials so that the password is stored securely.
     * 
     * This does require an extra http request when instantiating
     * the service which adds to latency. Alternatively, you could
     * store the generated access token in a custom setting and simply
     * reference it from your code, but then anyone who can view
     * custom settings can view the access token and use the API.
     * Trade-offs.
     */
    private String getAccessToken() {
        
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:Bitly/oauth/access_token');
        req.setMethod('POST');
        
        Http http = new Http();
        HttpResponse res = http.send(req);
        return res.getBody();
    }
    
}



BitlyShortenURLInvocable APEX Class:
public class BitlyShortenURLInvocable {
    
    @InvocableMethod(
        label = 'shorten'
        description = 'Given service request IDs then generates a bitly short url for them'
    )
    public static List<String> shorten( List<ID> srIds ) {
        
        // You can't invoke http callouts from Process Builder or Flow
        // because the database transaction has not committed yet, you will get error:
        // "System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out"
        // To get around this then we'll actually make the call in a @Future method asynchronously.
        shortenAsync( srIds );
        
    return new List<String>();
    }
    
    @Future( callout = true )
    private static void shortenAsync( List<ID> srIds ) {
        
        // Fetch your data and a field where you want to store the generated short url
        List<Service_Request__c> requests = new List<Service_Request__c>([ SELECT id, assigned_to_text__c, short_url__c FROM service_request__c WHERE id IN :srIds ]);
        
        // Service to actually call out to bitly and get a shortened url
    BitlyService service = new BitlyService();
        
        // Bitly does not support bulk url shortening
        // so you must make each call individually.
        // Do be aware of Bitly's rate limiting if you try
        // to mass create a bunch of records in short succession
        // http://dev.bitly.com/rate_limiting.html
        for ( Service_Request__c ServiceRequestObj : requests ) {
            
            // create Service Request URL to be shortened (with the variables needed by the Service Request Status Flow)
            ServiceRequestObj.short_url__c = service.shorten( 'https://MyServerName.salesforce.com/My_Flow_Name_Here?varSRID=' + ServiceRequestObj.id + '&varOriginalAssignedTo=' + ServiceRequestObj.assigned_to_text__c );
            
        }
        
        // update the records with their short urls
        // use workflow or trigger to fire off the short url field being populated
        // to then send email alerts, etc. including the short url
        if ( requests.size() > 0 ) {
            update requests;
        }
        
    }
    
}






 
Best Answer chosen by Derek Davis 7
Derek Davis 7Derek Davis 7
Hello All! Below is what worked for test coverage:
 
@isTest
private class BitlyShortenURLInvocableTest {

    
    @isTest
    static void test_shorten2() {
        
        // This test just ensures the invocable class will run.
        // If we were to have an active process or trigger then
        // inserting the record would be sufficient, as in the above test method.
        // If not, then this method will be sufficient for code coverage.
        // 
        // However, do note that trying to do both in the same test will fail.
        // Trying to insert a record and call the invocable class will fail
        // because the invocable class calls a @Future method that makes an HttpCallout.
        // DML operations don't mix with HttpCallouts due to uncommitted work:
        // "System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out.""
        
        Test.startTest();
        
        Test.setMock( HttpCalloutMock.class, new BitlyHttpCalloutMock() );
        
        BitlyShortenURLInvocable.shorten( new String[] {} );
        
        Test.stopTest();
        
    }
    
}
 
@isTest
private class BitlyServiceTest {

    @isTest
    static void test_shorten() {

        Test.startTest();

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

        BitlyService service = new BitlyService();

        String shortURL = service.shorten( URL.getSalesforceBaseUrl().toExternalForm() ) ;

        Test.stopTest();

    }

}


 
@isTest
public class BitlyHttpCalloutMock implements HttpCalloutMock {

    public HttpResponse respond( HttpRequest req ) {

        String endpoint = req.getEndpoint();

        if ( endpoint.contains('/oauth/access_token') ) {
            return buildOAuthResponse( req );
        } else if ( endpoint.contains('/v3/shorten') ) {
            return buildShortenResponse( req );
        }

        return null;
    }

    private HttpResponse buildOAuthResponse( HttpRequest req ) {

        HttpResponse res = new HttpResponse();

        res.setBody('123');
        res.setStatusCode(200);

        return res;
    }

    private HttpResponse buildShortenResponse( HttpRequest req ) {

        HttpResponse res = new HttpResponse();

        res.setBody('https://www.salesforce.com');
        res.setStatusCode(200);

        return res;
    }
    
}



 

All Answers

Ankit Gupta@ DeveloperAnkit Gupta@ Developer
Hi Derek Davis 7,

In BitlyService  you ar making http callout . You will have to create a test class with HttpCalloutMock .

Kindly follow the below link:

https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_restful_http_testing_httpcalloutmock.htm

https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_restful_http_testing_dml.htm

Regards
Ankit Gupta
RohRoh
Hello Derek Davis 7,
I agree with what Ankit has said, for testing HTTP callouts its always better to have your Mock Callout Classes.
However, there is a way to cover part of this class,
for example:

public static testmethod void negativeTest1() {
      Test.startTest();
      String string123 = '123123123123';
      MDTBitlyConnection bitlyObject= new MDTBitlyConnection();
      MDTBitlyConnection.accessToken = null;
      String shortenURL = bitlyObject.getShortenURL(string123);
        
      Test.stopTest();
   }

PLEASE VOTE THIS AS THE RIGHT ANSWER, IF YOU LIKE IT.

Thanks,
Rohit Alladi
Derek Davis 7Derek Davis 7
I attempted the following MockHttpResponseGenerator... it didn't seem to do anything. Can anyone tell me what I am missing?

Thanks!
@isTest
 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.assetEquals('callout:Bitly/oauth/access_token', req.getEndpoint());
   //System.assertEquals('POST', req.postMethod());
    
      //Begin Fake Response
        HttpResponse res = new HttpResponse();
        res.setHeader('Content-Type','application/json');
        res.setBody('{"foo":"bar"}');
        res.setStatusCode(200);
       return res;
           }
           }   
	 //End Fake Response

 
Derek Davis 7Derek Davis 7
Hello All! Below is what worked for test coverage:
 
@isTest
private class BitlyShortenURLInvocableTest {

    
    @isTest
    static void test_shorten2() {
        
        // This test just ensures the invocable class will run.
        // If we were to have an active process or trigger then
        // inserting the record would be sufficient, as in the above test method.
        // If not, then this method will be sufficient for code coverage.
        // 
        // However, do note that trying to do both in the same test will fail.
        // Trying to insert a record and call the invocable class will fail
        // because the invocable class calls a @Future method that makes an HttpCallout.
        // DML operations don't mix with HttpCallouts due to uncommitted work:
        // "System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out.""
        
        Test.startTest();
        
        Test.setMock( HttpCalloutMock.class, new BitlyHttpCalloutMock() );
        
        BitlyShortenURLInvocable.shorten( new String[] {} );
        
        Test.stopTest();
        
    }
    
}
 
@isTest
private class BitlyServiceTest {

    @isTest
    static void test_shorten() {

        Test.startTest();

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

        BitlyService service = new BitlyService();

        String shortURL = service.shorten( URL.getSalesforceBaseUrl().toExternalForm() ) ;

        Test.stopTest();

    }

}


 
@isTest
public class BitlyHttpCalloutMock implements HttpCalloutMock {

    public HttpResponse respond( HttpRequest req ) {

        String endpoint = req.getEndpoint();

        if ( endpoint.contains('/oauth/access_token') ) {
            return buildOAuthResponse( req );
        } else if ( endpoint.contains('/v3/shorten') ) {
            return buildShortenResponse( req );
        }

        return null;
    }

    private HttpResponse buildOAuthResponse( HttpRequest req ) {

        HttpResponse res = new HttpResponse();

        res.setBody('123');
        res.setStatusCode(200);

        return res;
    }

    private HttpResponse buildShortenResponse( HttpRequest req ) {

        HttpResponse res = new HttpResponse();

        res.setBody('https://www.salesforce.com');
        res.setStatusCode(200);

        return res;
    }
    
}



 
This was selected as the best answer