Sunday, October 20, 2019

Mock Framework

As Salesforce has introduced powerful mock framework for testing, we wrote thin layer on top of it to take full advantage of the dynamic mock class that you can create.



It is always a good practice to break down your classes, and methods into small chunks, and mostly take further reusable methods in services. However, at the time of testing, it gets hard to test your code if it relies on too many services method. You have to have to knowledge of the service.

In code, it may look like


/**
 * Created by Chintan Shah on 10/18/2019.
 */

/**
 * this is the service that will be called by caller
 * it would be faked out during the testing framework.
 */
public with sharing class MockDemoService {

    public List<String> toUpper(List<String> inputs) {
        List<String> outputs = new List<String>();
        for(String input : inputs ) {
            outputs.add( input.toUpperCase() );
        }
        return outputs;
    }

    public String concat(List<String> inputs) {
        String output = null;
        for(String input : inputs ) {
            output = ( output == null ? '' : ( output + ' ' ) ) + input;
        }
        return output;
    }


}


And caller


/**
 * Created by Chintan Shah on 10/18/2019.
 */

/**
 * demo of how to use this framework.
 *
 * MockDemoServiceCaller -> MockDemoService
 *
 * During testing, we will just mock the entire MockDemoService
 *
 */
public with sharing class MockDemoServiceCaller {

    /**
     * instance of mockDemoService
     */
    public MockDemoService mockDemoService;

    /**
     * regular constructor
     */
    public MockDemoServiceCaller() {
        mockDemoService = new MockDemoService();
    }

    /**
     * entry point for fake service
     *
     * @param
     */
    public MockDemoServiceCaller(MockDemoService mockDemoService) {
        this.mockDemoService = mockDemoService;
    }


    /**
     * sample routine that we are going to test.
     */
    public String sampleRoutine(List<String> inputs) {
        try {
            System.debug(' MockDemoServiceCaller.sampleRoutine inputs ' + JSON.serialize(inputs) );
            List<String> outputs = mockDemoService.toUpper( new List<String> { 'John', 'Doe' } );
            System.debug(' MockDemoServiceCaller.sampleRoutine outputs ' + JSON.serialize(outputs) );
            String output = mockDemoService.concat( outputs );
            System.debug(' MockDemoServiceCaller.sampleRoutine output ' + output );
            return output;
        } catch(Exception e) {
            throw new MockException('What\' up? ' + e.getStackTraceString() );
        }
    }


}

Now when we write the test class for MockDemoServiceCaller, it would be good to have full control over the output of mock demo service. We should be able to return various result including the exception. Salesforce mock framework allows you to mock the service so you have full control when you write the test class for service caller.



Here is example of the code. You can see in line 19 and 20, we create dynamic service instance using mock framework, as well as send mock individual method and return the response.

On line 34, we can see, we can even return the exception in the dynamic mock instance that we create, and hence we have full control on how to test the service caller.



 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
 * Created by Chintan Shah on 10/18/2019.
 */

/**
 * test class for MockDemoServiceCaller
 */

@isTest
public with sharing class MockDemoServiceCallerTest {

    @testSetup
    public static void testSetup() {

    }

    @isTest
    public static void testSampleRoutine1() {
        MockProvider mockDeMockServiceProvider = new MockProvider( MockDemoService.class );
        MockDemoService mockDemoService = (MockDemoService) mockDeMockServiceProvider.getMock();
        mockDeMockServiceProvider.addReturnValue( 'toUpper',  new List<String> { 'HELLO', 'WORLD' } );
        mockDeMockServiceProvider.addReturnValue( 'concat',  'HELLO TEST' );
        // initialize MockDemoServiceCaller with mock MockDemoService
        MockDemoServiceCaller mockDemoServiceCaller = new MockDemoServiceCaller( mockDemoService );
        String output = mockDemoServiceCaller.sampleRoutine( new List<String> { 'John', 'Doe'} );
        System.assertEquals( output, 'HELLO TEST', 'It should be HELLO TEST based on mock ');
    }

    @isTest
    public static void testSampleRoutine2() {
        MockProvider mockDeMockServiceProvider = new MockProvider( MockDemoService.class );
        MockDemoService mockDemoService = (MockDemoService) mockDeMockServiceProvider.getMock();
        mockDeMockServiceProvider.addReturnValue( 'toUpper',  new List<String> { 'HELLO', 'WORLD' } );
        mockDeMockServiceProvider.addReturnValue( 'concat',  new MockException('Test Exception') );
        // initialize MockDemoServiceCaller with mock MockDemoService
        MockDemoServiceCaller mockDemoServiceCaller = new MockDemoServiceCaller( mockDemoService );
        try {
            String output = mockDemoServiceCaller.sampleRoutine( new List<String> { 'John', 'Doe'} );
            System.assert( false, 'Exception was expected, success was not expected ');
        } catch(Exception e) {
            System.assert( true, 'Exception was expected. ');
        }
    }


}

With wrapper around salesforce framework, we don't need to write individual mock classes, but we can declarative use what to mock and what it would return.


Web Service Mock
Web service mock framework has been around for years, however we have to write mock class for each web service we are trying to call. With this framework, we can declarative create dynamic instance of the mock class and test the class which is calling web service. e.g. below




Actual Class


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
 * Created by Chintan Shah on 10/19/2019.
 */

public with sharing class MockDemoWebServiceCaller {

    public List<Map<String,Object>> httpGetCallout(String url) {
        List<Map<String,Object>> returnEmployees = new List<Map<String,Object>>();
        Http http = new Http();
        HttpRequest request = new HttpRequest();
        request.setEndpoint( url );
        request.setMethod('GET');
        System.debug(' MockDemoWebServiceCaller.httpGetCallout url ' + url );
        HttpResponse response = http.send(request);
        if (response.getStatusCode() == 200) {
            System.debug(' response.getBody() ' + response.getBody() );
            List<Object> employees = (List<Object>) JSON.deserializeUntyped( response.getBody() );
            for(Object employee : employees ) {
                Map<String, Object> currentEmployee = ( Map<String, Object> ) ( employee );
                returnEmployees.add( currentEmployee );
            }
        } else {
            throw new MockException( response.getStatusCode() + ' ' + response.getBody() );
        }
        system.debug( ' MockDemoWebServiceCaller.httpGetCallout returnEmployees ' + returnEmployees  );
        return returnEmployees;
    }

    public List<Map<String,Object>> getAllEmployees() {
        try {
            return httpGetCallout('https://basic-authentication-ws.herokuapp.com/hr/employees');
        } catch(Exception e) {
            throw new MockException(e);
        }
    }

    public List<Map<String,Object>> getEmployee(String column, String value) {
        try {
            return httpGetCallout('https://basic-authentication-ws.herokuapp.com/hr/employee/' + column + '/' + value );
        } catch(Exception e) {
            throw new MockException(e);
        }
    }


}


Test Class


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
 * Created by Chintan Shah on 10/19/2019.
 */

@isTest
public with sharing class MockDemoWebServiceCallerTest {
    @testSetup
    public static void testSetup() {

    }

    @isTest
    public static void testGetAllEmployees() {
        MockHttpCallout mockHttpCallout = new MockHttpCallout();
        mockHttpCallout.setMock();
        mockHttpCallout.addJsonHttpResponse('https://basic-authentication-ws.herokuapp.com/hr/employees', '[{"ExternalId":"E-001","Name":"Chintan Shah","DisplayUrl":"https://external.com/E-001","EmployeeAccountKey":"Account-001"},{"ExternalId":"E-002","Name":"Chuck Summer","DisplayUrl":"https://external.com/E-002","EmployeeAccountKey":"Account-001"},{"ExternalId":"E-003","Name":"Chris Walker","DisplayUrl":"https://external.com/E-003","EmployeeAccountKey":"Account-001"},{"ExternalId":"E-004","Name":"Dan Topper","DisplayUrl":"https://external.com/E-004","EmployeeAccountKey":"Account-002"},{"ExternalId":"E-005","Name":"Drew Berry","DisplayUrl":"https://external.com/E-005","EmployeeAccountKey":"Account-002"},{"ExternalId":"E-006","Name":"Don Aiken","DisplayUrl":"https://external.com/E-006","EmployeeAccountKey":"Account-002"}]');
        System.assertEquals( 6, new MockDemoWebServiceCaller().getAllEmployees().size(), 'Should be 6');
    }

    @isTest
    public static void testGetEmployee() {
        //
        MockHttpCallout mockHttpCallout = new MockHttpCallout();
        mockHttpCallout.setMock();
        mockHttpCallout.addJsonHttpResponse('https://basic-authentication-ws.herokuapp.com/hr/employee/Name/Chintan', '[{"ExternalId":"E-001","Name":"Chintan Shah","DisplayUrl":"https://external.com/E-001","EmployeeAccountKey":"Account-001"}]');
        System.assertEquals( 1, new MockDemoWebServiceCaller().getEmployee('Name','Chintan').size() , 'Should be 1');
    }

    @isTest
    public static void testGetAllEmployees2() {
        MockHttpCallout mockHttpCallout = new MockHttpCallout();
        mockHttpCallout.setMock();
        mockHttpCallout.addErrorResponse('https://basic-authentication-ws.herokuapp.com/hr/employees', 401 );
        try {
            new MockDemoWebServiceCaller().getAllEmployees().size();
            System.assert( false, 'Exception was expected, success was not expected ');
        } catch(Exception e) {
            System.assert( true, 'Exception was expected. ');
        }
    }

    @isTest
    public static void testGetEmployee2() {
        //
        MockHttpCallout mockHttpCallout = new MockHttpCallout();
        mockHttpCallout.setMock();
        mockHttpCallout.addErrorResponse('https://basic-authentication-ws.herokuapp.com/hr/employee/Name/Chintan', 401);
        try {
            new MockDemoWebServiceCaller().getEmployee('Name','Chintan').size();
            System.assert( false, 'Exception was expected, success was not expected ');
        } catch(Exception e) {
            System.assert( true, 'Exception was expected. ');
        }
    }


}


As you can see, we don't have to write the mock class for each web service, we simply just need to use the http mock callout and set the response as mentioned in line 25 and 33.

You can download the code from : https://github.com/springsoa/springsoa-salesforce
or from unmanaged package here




No comments: