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
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
public interface ZCMockMethodRecorder {
ZCMockMethodRecorder thenReturn(Object result);
ZCMockMethodRecorder thenException(Exception ex);
}
Basic steps of usage
Create an instance of ZCMock
Create a mock using ZCMock instance
Inject mock to an object to test
Stubbing mock methods
Start test
Validate test
1. Create an instance of ZCMock
zsfjira.ZCMock zcMock = new zsfjira.ZCMock();
2. Create a mock using ZCMock instance
zsfjira.ZCIssueApi zcIssueApi = (zsfjira.ZCIssueApi)zcMock.mock(zsfjira.ZCIssueApi.class);
3. Inject mock to an object to test
zsfjira.ZC.Issues = zcIssueApi;
4. Stubbing mock methods
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
zsfjira.ZCBeans.IssueUpsertResult result = zsfjira.ZC.Issues.createIssue(issueTemplate);
6. Validate test
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
zsfjira.ZC.Issues.createJIRAIssueComment(zcMock.eqString('TP-11'), zcMock.anyString())
Stubbed Object | zsfjira.ZC.Issues |
Stubbed Object Method | createJIRAIssueComment |
Any parameter | zcMock.anyString() |
Eq parameter | zcMock.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.
@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
@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:
@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.
@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
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);
}
}
}
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});
}
}
}
}
@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.
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
}));
}
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
})
);
}
}
}
}
}
}
@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);
}
}