Skip to main content
Skip table of contents

Write unit tests for zAgileConnect API using ZCMock

ZCMock is a development utility to build unit tests for zAgileConnect API. ZCMock can instance stubbed objects of zAgileConnect API classes

ZCMock Api classes

ZCMock

Utility to create mocks of a Class

Interface

CODE
interface ZCMock extends StubProvider{
    Object mock(Type ty);
    Object mock(Type ty, StubProvider sp);
    ZCMockMethodRecorder when(Object input);
    ZCMockMethodRecorder when();
    void startStubbing();
    void stopStubbing();
    Boolean verify(Object stubbedObject, String methodName, List<Type> listOfTypes, List<Object> listOfArgs);
    Object any();
    List<Object> anyList();
    Set<Object> anySet();
    Map<Object,Object> anyMap();
    Blob anyBlob();
    Boolean anyBoolean();
    Date anyDate();
    Datetime anyDatetime();
    Decimal anyDecimal();
    Double anyDouble();
    Id anyId();
    Integer anyInteger();
    Long anyLong();
    String anyString();
    Object eq(Object obj);
    List<Object> eqList(List<Object> lst);
    Set<Object> eqSet(Set<Object> s);
    Map<Object,Object> eqMap(Map<Object, Object> m);
    Blob eqBlob(Blob b);
    Boolean eqBoolean(Boolean b);
    Date eqDate(Date d);
    Datetime eqDatetime(Datetime dt);
    Decimal eqDecimal(Decimal d);
    Double eqDouble(Double d);
    Id eqId(Id i);
    Integer eqInteger(Integer i);
    Long eqLong(Long lo);
    String eqString(String str);
}

ZCMockRecorder

Utility to record outputs of a method for a stubbed object.

Interface

CODE
public interface ZCMockMethodRecorder {
    ZCMockMethodRecorder thenReturn(Object result);
    ZCMockMethodRecorder thenException(Exception ex);
}

Basic steps of usage

  1. Create an instance of ZCMock

  2. Create a mock using ZCMock instance

  3. Inject mock to an object to test

  4. Stubbing mock methods

  5. Start test

  6. Validate test

1. Create an instance of ZCMock

CODE
zsfjira.ZCMock zcMock = new zsfjira.ZCMock();

2. Create a mock using ZCMock instance

CODE
zsfjira.ZCIssueApi zcIssueApi = (zsfjira.ZCIssueApi)zcMock.mock(zsfjira.ZCIssueApi.class);

3. Inject mock to an object to test

CODE
zsfjira.ZC.Issues = zcIssueApi;

4. Stubbing mock methods

CODE
String testIssueKey = 'TP-11';
zsfjira.ZCBeans.IssueTemplate issueTemplate = new zsfjira.ZCBeans.IssueTemplate(testIssueKey);

zcMock.startStubbing();
zcMock.when(zsfjira.ZC.Issues.createIssue(issueTemplate))
        .thenReturn(new zsfjira.ZCBeans.IssueUpsertResult(issueTemplate));
zcMock.stopStubbing();

5. Start test

CODE
zsfjira.ZCBeans.IssueUpsertResult result = zsfjira.ZC.Issues.createIssue(issueTemplate);

6. Validate test

CODE
System.assertEquals(testIssueKey, result.getIssueKey());

Features

Any/Eq match parameters

ZCMock instances Stubbed objects of a given class, then you can define outputs for an exact input by using "eq", for cases where you don’t know the exact input you can use the "any" parameter

If you use one ZCMock::any method parameter you must use ZCMock::eq or ZCMock::any for the remained parameters

Sample

CODE
zsfjira.ZC.Issues.createJIRAIssueComment(zcMock.eqString('TP-11'), zcMock.anyString())
Stubbed Objectzsfjira.ZC.Issues
Stubbed Object MethodcreateJIRAIssueComment
Any parameterzcMock.anyString()
Eq parameterzcMock.eqString('TP-11')


Sample

This sample mocks ZCApi::createJIRAIssueComment method. The issue key is known to be "TP-11" but the comment value can be any value.

CODE
@IsTest
public with sharing class client_ZCMockTest_any {

    @IsTest
    static void testAnyArguments(){
        zsfjira.ZCMock zcMock = new zsfjira.ZCMock();

        zsfjira.ZC.Issues = (zsfjira.ZC.IssueApi) zcMock.mock(zsfjira.ZCIssueApi.class);

        zsfjira.ZCBeans.IssueCommentResult commentResult = new zsfjira.ZCBeans.IssueCommentResult('TP-11', 'commentId');

        zcMock.startStubbing();
        // use of eq is mandatory because we are using an any
        zcMock.when(zsfjira.ZC.Issues.createJIRAIssueComment(zcMock.eqString('TP-11'), zcMock.anyString())).thenReturn(commentResult);
        zcMock.stopStubbing();

        System.assertEquals(zsfjira.ZC.Issues.createJIRAIssueComment('TP-11', 'comment content'), commentResult);
    }
}

Verify method was invoked

After invoking a function, you may want to validate if one or many inner functions were called among the first invocation, use ZCMock::verify in this scenario.

Sample

This sample mocks ZCApi::createJIRAIssueComment method and asserts if the method was invoked by using ZCMock::verify

CODE
@IsTest
public with sharing class client_ZCMockTest_verify {

    @IsTest
    static void testVerify(){
        zsfjira.ZCMock zcMock = new zsfjira.ZCMock();
        zsfjira.ZC.Issues = (zsfjira.ZC.IssueApi) zcMock.mock(zsfjira.ZCIssueApi.class);

        zcMock.startStubbing();
        // use of eq is mandatory because we are using an any
        zcMock.when(zsfjira.ZC.Issues.createJIRAIssueComment(zcMock.eqString('TP-11'), zcMock.anyString()))
                .thenReturn(new zsfjira.ZCBeans.IssueCommentResult('', ''));
        zcMock.stopStubbing();

        System.assert(!zcMock.verify(zsfjira.ZC.Issues, 'createJIRAIssueComment', new List<Type>{String.class, String.class}, new List<Object>{'TP-11', zcMock.anyString()}));
        zsfjira.ZC.Issues.createJIRAIssueComment('TP-11', 'comment content');
        System.assert(zcMock.verify(zsfjira.ZC.Issues, 'createJIRAIssueComment', new List<Type>{String.class, String.class}, new List<Object>{'TP-11', zcMock.anyString()}));
    }

}


Testing Void Methods

If a Method doesn’t return a value(void) It isn’t required to mock it. you can verify the method was executed by using ZCMock::verify

Sample

The method ZCApi::postErrorMessage returns void, then it can be tested as following:

CODE
@IsTest
public with sharing class client_ZCMockTest_verifyVoidMethod {

    @IsTest
    static void testVerifyVoidMethod(){
        zsfjira.ZCMock zcMock = new zsfjira.ZCMock();
        zsfjira.ZC.Issues = (zsfjira.ZC.IssueApi) zcMock.mock(zsfjira.ZCIssueApi.class);

        Case ca = new Case();
        insert ca;

        System.assert(!zcMock.verify(zsfjira.ZC.Issues, 'postErrorMessages',
                new List<Type>{List<zsfjira.ZCBeans.GenericErrorMessage>.class}, new List<Object>{zcMock.any()}));

        zsfjira.ZC.Issues.postErrorMessages(new List<zsfjira.ZCBeans.GenericErrorMessage>{
                new zsfjira.ZCBeans.GenericErrorMessage(ca.Id, 'a message')
        });

        System.assert(zcMock.verify(zsfjira.ZC.Issues, 'postErrorMessages',
                new List<Type>{List<zsfjira.ZCBeans.GenericErrorMessage>.class}, new List<Object>{zcMock.any()}));

    }
}


Custom stub provider

ZCMock is used for creating Stubbed Objects only. If you have a use case isn’t covered by ZCMock, then, It is possible to build unit tests with a Custom Stub Provider. Use a Custom Stub Provider to return mock responses.

Sample

In this example, for any method calles for ZC.Issues the return value will be force to return a issue upsert result. refer to https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_interface_System_StubProvider.htm for further details about the stub provider interface.

CODE
@IsTest
public with sharing class client_ZCMockTest_customStubProvider {

    static String testIssueKey = 'TP-11';

    @IsTest
    static void testCustomStubProvider(){
        zsfjira.ZCMock zcMock = new zsfjira.ZCMock();

        zsfjira.ZC.Issues = (zsfjira.ZC.IssueApi)zcMock.mock(zsfjira.ZCIssueApi.class,
                        new CustomStubProvider());

        zsfjira.ZCBeans.IssueUpsertResult result = zsfjira.ZC.Issues.createIssue(null);

        System.assertEquals(testIssueKey, result.getIssueKey());
    }

     class CustomStubProvider implements StubProvider{
        public Object handleMethodCall(Object stubbedObject, String stubbedMethodName, Type returnType,
                List<Type> listOfParamTypes, List<String> listOfParamNames, List<Object> listOfArgs) {
            return new zsfjira.ZCBeans.IssueUpsertResult(new zsfjira.ZCBeans.IssueTemplate(testIssueKey));
        }
    }
}


Trigger example 1: On Case Close then Close Issue

The sample closes a Jira Issue when a linked SF Case is closed. If an error is raised when closing the Issue it will be posted on the Case.

The test cover Success and Failure cases

CODE
trigger client_OnCaseClosedJiraIssuesTrigger on Case (after update) {
    List<Case> cases = new List<Case>();

    for(Case oldCase: Trigger.old){
        Case newCase = Trigger.newMap.get(oldCase.Id);
        if(oldCase.Status != newCase.Status && newCase.Status == 'Closed'){
            cases.add(oldCase);
        }
    }

    for(Case ca: cases){
        for ( zsfjira__ZIssue_SF__c issueSf :[SELECT zsfjira__ZIssue__r.zsfjira__IssueKey__c FROM zsfjira__ZIssue_SF__c WHERE zsfjira__Case__c = :ca.Id] ){
            client_CaseClosedJiraIssuesHandler.closeIssue(ca.Id, issueSf.zsfjira__ZIssue__r.zsfjira__IssueKey__c);
        }
    }
}
CODE
public class client_CaseClosedJiraIssuesHandler {

    @Future (Callout = true)
    public static void closeIssue(Id caseId, String issueKey){
        zsfjira.ZCBeans.AvailableTransitionsResult availableTransitions = zsfjira.ZC.Issues.getAvailableTransitions(issueKey);

        if(availableTransitions.getAvailableTransitionsNames().contains('Close Issue')){
            zsfjira.ZCBeans.TransitionIssueBean transitionIssueBean = availableTransitions.buildIssueTransitionFromName('Close Issue');
            transitionIssueBean.setFieldByName('Resolution', new Map<String, String>{'name' => 'Done'});
            transitionIssueBean.setComment('Closed by case closed');
            zsfjira.ZCBeans.TransitionIssueResult transitionIssueResult = zsfjira.ZC.Issues.transitionIssue(issueKey, transitionIssueBean);

            if(transitionIssueResult.hasError()){
                zsfjira.ZCBeans.GenericErrorMessage errorMessage =
                        new zsfjira.ZCBeans.GenericErrorMessage(caseId, transitionIssueResult.getErrorMessage());
                zsfjira.ZC.Issues.postErrorMessages(new List<zsfjira.ZCBeans.GenericErrorMessage>{errorMessage});
            }

        }
    }
}
CODE
@IsTest
public class client_CaseClosedJiraIssuesHandlerTest {

    private static String testCaseSubject = 'Test Case';
    private static Integer testIssueId = 1001;
    private static String testIssueKey = 'TP-11';

    @TestSetup
    static void setup(){
        Case ca = new Case(Origin = 'Email', Subject = testCaseSubject);
        insert ca;

        zsfjira__ZIssue__c zIssue = new zsfjira__ZIssue__c(
                zsfjira__IssueId__c = testIssueId,
                zsfjira__IssueKey__c = testIssueKey
        );
        insert zIssue;

        zsfjira__ZIssue_SF__c zIssueSF = new zsfjira__ZIssue_SF__c(
                zsfjira__ZIssue__c = zIssue.Id,
                zsfjira__Case__c = ca.Id
        );
        insert zIssueSF;
    }

    static zsfjira.ZCMock configZCMock(){
        zsfjira.ZCMock zcMock = new zsfjira.ZCMock();

        zsfjira.ZC.Issues = (zsfjira.ZCIssueApi)zcMock.mock(zsfjira.ZCIssueApi.class);
        zsfjira.ZC.IssueFactory = (zsfjira.ZCIssueFactory)zcMock.mock(zsfjira.ZCIssueFactory.class);

        return zcMock;
    }

    @IsTest
    static void transitionIssueTest(){
        zsfjira.ZCMock zcMock = configZCMock();

        String availableTransitionsString = '{"expand": "transitions", "transitions": [{"name":  "Open"}, {"name": "Close Issue", "id": 10, "fields": {"resolution": {"name": "Resolution"}}}]}';

        zsfjira.ZCBeans.AvailableTransitionsResult availableTransitions = new zsfjira.ZCBeans.AvailableTransitionsResult(
                (Map<String, Object>)JSON.deserializeUntyped(availableTransitionsString)
                ,null
        );
        zsfjira.ZCBeans.TransitionIssueResult transitionIssueResult = new zsfjira.ZCBeans.TransitionIssueResult(testIssueKey, null);

        zcMock.startStubbing();
        zcMock.when(zsfjira.ZC.Issues.getAvailableTransitions(zcMock.anyString()))
                .thenReturn(availableTransitions);
        zcMock.when(zsfjira.ZC.Issues.transitionIssue(zcMock.anyString(), (zsfjira.ZCBeans.TransitionIssueBean)zcMock.any()))
                .thenReturn(transitionIssueResult);
        zcMock.stopStubbing();

        Case ca = [SELECT Id FROM Case WHERE Subject=:testCaseSubject LIMIT 1];

        Test.startTest();
        ca.Status = 'Closed';
        update ca;
        Test.stopTest();
    }
}

Trigger example 2: Creating Issues in bulk when creating Cases

The sample creates a Jira Issue when SF Cases are created. The Issue is created using the mapped fields in the zAgileConnect configuration Step 6.

CODE
trigger CreateIssues on Case (after insert) {
    Id jobId = CreateIssuesHandler.createIssues(Trigger.new);
    System.debug(System.LoggingLevel.INFO, 
                 String.format('Job for creating issues was submitted{0}', new List<Object> {
                     jobId
                         }));
}
CODE
public class CreateIssuesHandler {
    public static Id createIssues(List<Case> lstCases){
        return System.enqueueJob(new CreateIssuesAsyncExecution(lstCases));
    }
    
    public class CreateIssuesAsyncExecution implements Queueable, Database.AllowsCallouts {
        private List<Case> lstCases;
        public CreateIssuesAsyncExecution(List<Case> lstCases){
            this.lstCases = lstCases;
        }
        public void execute(QueueableContext context) {
            List<Id> lstIds = new List<Id>();
            for(Case currCase:lstCases){
                lstIds.add(currCase.Id);
            }
            List<zsfjira.ZCBeans.IssueTemplate> issueTemplates = 
                zsfjira.ZC.IssueFactory.buildCreateTemplateFromMapping(lstIds);
            
            List<zsfjira.ZCBeans.IssueUpsertResult> iresults = zsfjira.ZC.Issues.bulkCreateIssues(issueTemplates);
            
            for(zsfjira.ZCBeans.IssueUpsertResult iresult: iresults){
                if(iresult.hasError()){
                    for(String errorMessage:iresult.getErrorMessages()){
                        System.debug(System.LoggingLevel.ERROR,
                                     String.format('Error creating issue {0}: {1}', new List<Object> {
                                         iresult.getIssueKey(), errorMessage
                                     })
                                    );
                    }
                }
            }
            
        }
    }
}
CODE
@isTest
public class CreateIssuesHandlerTest {
    @isTest
    public static void testCreateIssues(){
        zsfjira.ZCMock zcMock = new zsfjira.ZCMock();
        
        zsfjira.ZC.IssueFactory = (zsfjira.ZCIssueFactory)zcMock.mock(zsfjira.ZCIssueFactory.class);
        zsfjira.ZC.Issues = (zsfjira.ZCIssueApi)zcMock.mock(zsfjira.ZCIssueApi.class);
        
        String testIssueKey = 'TP-11';
        zsfjira.ZCBeans.IssueTemplate issueTemplate = new zsfjira.ZCBeans.IssueTemplate(testIssueKey);
        
        zcMock.startStubbing();
        zcMock.when(zsfjira.ZC.IssueFactory.buildCreateTemplateFromMapping((List<Id>)zcMock.any())
                   ).thenReturn(new List<zsfjira.ZCBeans.IssueTemplate>{
                       new zsfjira.ZCBeans.IssueTemplate(testIssueKey)
                           });
        
        zsfjira.ZCBeans.IssueUpsertResult iresult = new zsfjira.ZCBeans.IssueUpsertResult(issueTemplate);
        iresult.setErrorMessages(new List<String>());//results has no errors
        zcMock.when(zsfjira.ZC.Issues.bulkCreateIssues((List<zsfjira.ZCBeans.IssueTemplate>)zcMock.any()))
            .thenReturn(
                new List<zsfjira.ZCBeans.IssueUpsertResult>{
                    iresult
                        }
            );
        
        zcMock.stopStubbing();
        
        System.Test.startTest();
        Id jobId = CreateIssuesHandler.createIssues(new List<Case>{
            new Case(Subject='Case 1')
                });
        System.Test.stopTest();
        
        System.assert(jobId!=null);
    }
}
JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.