/** * File Name: fflib_TriggerHandlerTest * Description: Unit Test class for fflib_TriggerHandler implementation * * @author architect ir. Wilhelmus G.J. Velzeboer * * Copyright (c), W.G.J. Velzeboer, * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above author notice, * this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * - Neither the name of the author nor the names of its contributors * may be used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ @IsTest(IsParallel=true) private class fflib_TriggerHandlerTest { private static Boolean handlerGotInvoked = false; @IsTest static void testBehavior() { // GIVEN a set of records with a configured trigger handler fflib_LogEntry__c record = new fflib_LogEntry__c(LogLevel__c = 'INFO', Message__c = 'Unit Test'); List records = new List {record}; fflib_TriggerAction__mdt triggerAction = new fflib_TriggerAction__mdt( ObjectType__c = null, ObjectTypeAlternate__c = Schema.fflib_LogEntry__c.SObjectType.getDescribe().getName(), BeforeInsert__c = true, ImplementationType__c = 'fflib_TriggerHandlerTest.MyTriggerAction', ExecutionContext__c = 'Realtime' ); fflib_TriggerHandler triggerHandler = new fflib_TriggerHandler(); String ctxClassName = triggerHandler.triggerContextType.getName() + '.Constructor'; fflib_TriggerContext ctx = (fflib_TriggerContext) ((fflib_IDomainConstructor) Type.forName(ctxClassName).newInstance()) .construct(new List {record}); ctx.triggerOperation = System.TriggerOperation.BEFORE_INSERT; // WHEN a record is inserted System.Test.startTest(); triggerHandler.loadConfiguration(); triggerHandler.addTriggerActionConfig(triggerAction); triggerHandler .setTriggerContextType(fflib_TriggerContext.class) .setTriggerContext(ctx) .run(); System.Test.stopTest(); // THEN the trigger handler should be invoked System.Assert.isTrue(handlerGotInvoked, 'Whoops, the Trigger Action was not invoked'); System.Assert.areEqual(records, ctx.records); System.Assert.isTrue(ctx.isInsert()); System.Assert.isTrue(ctx.isBefore()); System.Assert.isFalse(ctx.isDelete()); System.Assert.isFalse(ctx.isUndelete()); } @IsTest private static void itShouldReturnTheChangedRecordsBySObjectFields() { // GIVEN a domain with old and changed records Id idLuke = fflib_IDGenerator.generate(Account.SObjectType); Id idHan = fflib_IDGenerator.generate(Account.SObjectType); Id idLeia = fflib_IDGenerator.generate(Account.SObjectType); List oldRecords = new List { new Account(Id = idLuke, Name = 'Luke', Description = 'Jedi'), new Account(Id = idHan, Name = 'Han', Description = 'Pilot'), new Account(Id = idLeia, Name = 'Leia') }; List newRecords = oldRecords.deepClone(true, true, true); newRecords.get(0).Name = 'Luke SkyWalker'; newRecords.get(0).Description = 'Jedi Master'; newRecords.get(1).Name = 'Han Solo'; fflib_TriggerContext ctx = new fflib_TriggerContext(newRecords); ctx.existingRecords = new Map(oldRecords); // WHEN we create a domain with ExistingRecords and request the changed records List result = ctx.getChangedRecords(Account.Name); // THEN it should only return the changed records Map resultMap = new Map(result); System.Assert.areEqual(2, result.size()); System.Assert.istrue(resultMap.containsKey(idLuke)); System.Assert.istrue(resultMap.containsKey(idHan)); } @IsTest private static void itShouldReturnTheChangedRecordsByStringFields() { // GIVEN a domain with old and changed records Id idLuke = fflib_IDGenerator.generate(Account.SObjectType); Id idHan = fflib_IDGenerator.generate(Account.SObjectType); Id idLeia = fflib_IDGenerator.generate(Account.SObjectType); List oldRecords = new List { new Account(Id = idLuke, Name = 'Luke', Description = 'Jedi'), new Account(Id = idHan, Name = 'Han', Description = 'Pilot'), new Account(Id = idLeia, Name = 'Leia') }; List newRecords = oldRecords.deepClone(true, true, true); newRecords.get(0).Name = 'Luke SkyWalker'; newRecords.get(0).Description = 'Jedi Master'; newRecords.get(1).Name = 'Han Solo'; fflib_TriggerContext ctx = new fflib_TriggerContext(newRecords); ctx.existingRecords = new Map(oldRecords); // WHEN we create a domain with ExistingRecords and request the changed records List result = ctx.getChangedRecords('Description'); // THEN it should only return the changed records Map resultMap = new Map(result); System.Assert.areEqual(1, result.size()); System.Assert.istrue(resultMap.containsKey(idLuke)); } @IsTest private static void itShouldCallTriggerActionOnAfterDelete() { List records = new List { new Account(Id = fflib_IDGenerator.generate(Account.SObjectType)) }; fflib_TriggerContext ctx = new fflib_TriggerContext(records); ctx.triggerOperation = System.TriggerOperation.AFTER_DELETE; new MyTriggerAction().setContext(ctx).doWork(); System.Assert.istrue(ctx.isAfter()); System.Assert.istrue(ctx.isDelete()); } @IsTest private static void itShouldCallTriggerActionOnAfterInsert() { List records = new List { new Account(Id = fflib_IDGenerator.generate(Account.SObjectType)) }; fflib_TriggerContext ctx = new fflib_TriggerContext(records); ctx.triggerOperation = System.TriggerOperation.AFTER_INSERT; new MyTriggerAction().setContext(ctx).doWork(); System.Assert.istrue(ctx.isAfter()); System.Assert.istrue(ctx.isInsert()); } @IsTest private static void itShouldCallTriggerActionOnAfterUndelete() { List records = new List { new Account(Id = fflib_IDGenerator.generate(Account.SObjectType)) }; fflib_TriggerContext ctx = new fflib_TriggerContext(records); ctx.triggerOperation = System.TriggerOperation.AFTER_UNDELETE; new MyTriggerAction().setContext(ctx).doWork(); System.Assert.istrue(ctx.isAfter()); System.Assert.istrue(ctx.isUndelete()); } @IsTest private static void itShouldCallTriggerActionOnAfterUpdate() { List records = new List { new Account(Id = fflib_IDGenerator.generate(Account.SObjectType)) }; fflib_TriggerContext ctx = new fflib_TriggerContext(records); ctx.triggerOperation = System.TriggerOperation.AFTER_UPDATE; new MyTriggerAction().setContext(ctx).doWork(); System.Assert.istrue(ctx.isAfter()); System.Assert.istrue(ctx.isUpdate()); } @IsTest private static void itShouldCallTriggerActionOnBeforeDelete() { List records = new List { new Account(Id = fflib_IDGenerator.generate(Account.SObjectType)) }; fflib_TriggerContext ctx = new fflib_TriggerContext(records); ctx.triggerOperation = System.TriggerOperation.BEFORE_DELETE; new MyTriggerAction().setContext(ctx).doWork(); System.Assert.istrue(ctx.isBefore()); System.Assert.istrue(ctx.isDelete()); } @IsTest private static void itShouldCallTriggerActionOnBeforeInsert() { List records = new List { new Account(Id = fflib_IDGenerator.generate(Account.SObjectType)) }; fflib_TriggerContext ctx = new fflib_TriggerContext(records); ctx.triggerOperation = System.TriggerOperation.BEFORE_INSERT; new MyTriggerAction().setContext(ctx).doWork(); System.Assert.istrue(ctx.isBefore()); System.Assert.istrue(ctx.isInsert()); } @IsTest private static void itShouldCallTriggerActionOnBeforeUpdate() { List records = new List { new Account(Id = fflib_IDGenerator.generate(Account.SObjectType)) }; fflib_TriggerContext ctx = new fflib_TriggerContext(records); ctx.triggerOperation = System.TriggerOperation.BEFORE_UPDATE; new MyTriggerAction().setContext(ctx).doWork(); System.Assert.istrue(ctx.isBefore()); System.Assert.istrue(ctx.isUpdate()); } @IsTest private static void itShouldUseTheStatefulTriggerActionClass() { fflib_LogEntry__c record = new fflib_LogEntry__c(LogLevel__c = 'INFO', Message__c = 'Unit Test'); fflib_ApexMocks mocks = new fflib_ApexMocks(); fflib_ITriggerAction triggerActionMock = (fflib_ITriggerAction) mocks.mock(fflib_ITriggerAction.class); mocks.startStubbing(); mocks.when(triggerActionMock.isQueueable()).thenReturn(false); mocks.when(triggerActionMock.isNotQueueable()).thenReturn(true); mocks.stopStubbing(); fflib_TriggerHandler.triggerActionStateByClass = new Map{ MyTriggerAction.class => triggerActionMock }; fflib_TriggerAction__mdt triggerAction = new fflib_TriggerAction__mdt( ObjectType__c = null, ObjectTypeAlternate__c = Schema.fflib_LogEntry__c.SObjectType.getDescribe().getName(), BeforeInsert__c = true, ImplementationType__c = 'fflib_TriggerHandlerTest.MyTriggerAction', ExecutionContext__c = 'Realtime', Stateful__c = true ); fflib_TriggerHandler triggerHandler = new fflib_TriggerHandler(); String ctxClassName = triggerHandler.triggerContextType.getName() + '.Constructor'; fflib_TriggerContext ctx = (fflib_TriggerContext) ((fflib_IDomainConstructor) Type.forName(ctxClassName).newInstance()) .construct(new List {record}); ctx.triggerOperation = System.TriggerOperation.BEFORE_INSERT; // WHEN a record is inserted System.Test.startTest(); triggerHandler.addTriggerActionConfig(triggerAction); triggerHandler.setTriggerContext(ctx).run(); System.Test.stopTest(); // THEN the stateful trigger action should be re-used ((fflib_ITriggerAction) mocks.verify(triggerActionMock)).doWork(); } @IsTest private static void itShouldNotExecuteBeforeDelete() { fflib_LogEntry__c record = new fflib_LogEntry__c(LogLevel__c = 'INFO', Message__c = 'Unit Test'); fflib_TriggerHandler.triggerActionStateByClass = new Map{ MyTriggerBypassedAction.class => new MyTriggerBypassedAction() }; fflib_TriggerAction__mdt triggerAction = new fflib_TriggerAction__mdt( ObjectType__c = null, ObjectTypeAlternate__c = Schema.fflib_LogEntry__c.SObjectType.getDescribe().getName(), BeforeDelete__c = true, ImplementationType__c = 'fflib_TriggerHandlerTest.MyTriggerBypassedAction', ExecutionContext__c = 'Realtime', Stateful__c = true ); fflib_TriggerHandler triggerHandler = new fflib_TriggerHandler(); String ctxClassName = triggerHandler.triggerContextType.getName() + '.Constructor'; fflib_TriggerContext ctx = (fflib_TriggerContext) ((fflib_IDomainConstructor) Type.forName(ctxClassName).newInstance()) .construct(new List {record}); ctx.triggerOperation = System.TriggerOperation.BEFORE_DELETE; // WHEN a record is inserted System.Test.startTest(); triggerHandler.addTriggerActionConfig(triggerAction); // Setting bypass & run trigger fflib_TriggerHandler.getTriggerEvent(MyTriggerBypassedAction.class).disableAll(); triggerHandler.setTriggerContext(ctx).run(); System.Test.stopTest(); } public class MyTriggerAction extends fflib_TriggerAction // Todo - rename handler to action { public override void onBeforeInsert() { fflib_TriggerHandlerTest.handlerGotInvoked = true; List records = triggerContext.getRecords(); } } public class MyTriggerBypassedAction extends fflib_TriggerAction { public override void onBeforeDelete() { System.Assert.fail('It should not have reach this point'); } } }