0

I have written a relatively simple trigger and I am now attempting to write a test class for this trigger. The trigger is as follows.

trigger MapDatesPA on Position_Employee__c (before insert,before update)
// Create trigger that runs when a new record is inserted or when a record is updated that maps the employee fields //of Home Study Start Date, Training Start and End Dates to Position Assoicate Object
{
//Prevent Iterative Loop 
if(checkRecursive.runOnce()){ 
              Map<Id,Employee__c> pm = new Map <Id,Employee__c>();
              Set<Id> empIds = new Set<Id>();
              for(Position_Employee__c posEmp: Trigger.new){
                if(posEmp.Employee__c  != null){
                  empIds.add(posEmp.Employee__c);
                }
              }
            for (Employee__c emp : [
                        SELECT Id, HOME_STUDY_START_DATE__c, Training_Start_Date__c, Training_End_Date__c
                        FROM Employee__c
                        WHERE Id IN :empIds
            ]) {
                if (!pm.containsKey(emp.Id)) {
                    pm.put(emp.id, emp);
                }
            }
       
            
  for(Position_Employee__c posemp: Trigger.new){
      if (pm.containsKey(posemp.Employee__c)) {
        posemp.Training_Start_Date__c = pm.get(posemp.Employee__c).Training_Start_Date__c;
        posemp.Training_End_Date__c = pm.get(posemp.Employee__c).Training_End_Date__c;
        posemp.Home_Study_Start_Date__c = pm.get(posemp.Employee__c).HOME_STUDY_START_DATE__c;
 
              }
             }  
            }  
           }

I have two custom objects, Employee, and Position Associate. Position Associate is an object that creates a mapping and record between an employee and a respective position that they hold. The trigger, runs any time a Position Associate is created or updated, that takes three fields, Training Start, Training End, and Home Study Start, and populates it with data from the employee object, from the employee that is mapped to the Position Associate record in question. The trigger itself is complete, however, there are other triggers on the Position, Position Associate and Employee object. Theses theses other triggers will throw up an error upon the creation of new records as they are validation triggers For instance on a position associate record there is a trigger that checks to ensure that the employee assigned does not have an overlapping assignment, or to check if the employee is terminated. This would require a great deal of manual effort to write a test class that accommodates the needs of triggers on three separate objects. Could I write a test class that ignores all the other triggers, just to test this one trigger?

1
  • Your conundrum is one of the many issues solved by moving to the Domain pattern (see Trailhead) and the unit testing the domain layer. This can be done without having to do any DML
    – cropredy
    May 8, 2021 at 22:33

2 Answers 2

4

In general, triggers (and workflow, process builder, validation rules, and flows) cannot be turned off. We can use things like Custom Metadata Types and Custom Settings to provide a way for us to turn those things on and off at will, but the point is that it's something that you need to add into your code (and it's generally easier to add it the earlier you are in the code-writing process).

Managing code dependencies and designing code in a way that it's (relatively) easy to test is a fairly deep rabbit hole. It's a lot to process if you try to absorb it all at once, so I think I'll stick to making two recommendations.

#1: Use a Trigger Framework

You may or may not have heard that it's a good idea to limit yourself to having only 1 trigger per SObject (because we can't guarantee which order they'll be executed in if there are more than one).

Beyond that, triggers themselves aren't really test-friendly. You can't really pass anything into triggers aside from the records themselves, and they can only be run by performing DML. To get around those things, it's recommended to have the logic in a separate class, and then have your trigger call that class.

Trigger frameworks are the embodiment of that ideology. There are more than a few out there (and maybe one day I'll finish my own trigger framework), but a couple of common choices are Kevin O'Hara's Trigger Framework and Chris Aldridge's Lightweight Trigger Framework. Both give you the ability to turn triggers on and off programmatically.

#2: Start thinking about Dependency Injection

In a nutshell, Dependency Injection (DI) is about making sure that a class or method allows whoever is using that class/method to provide it with the things it needs to be able to run (instead of creating them inside the class/method itself).

Consider this rather contrived example without DI

public class MyClass{
    public Integer getValueFromElsewhere(){
        SomeOtherClass soc = new SomeOtherClass();

        return soc.blackBoxIntegerMachine();
    }
}

The problem here is that you don't have control over what SomeOtherClass does or how it's set up. On its own, under normal circumstances, blackBoxIntegerMachine() will hopefully not throw an exception.

...but what if you need to test what happens if blackBoxIntegerMachine() does throw an exception? How could you even make it throw an exception only for this test?

If getValueFromElsewhere() takes a SomeOtherClass as an argument, then you gain control over what happens

public class MyClass{
    public Integer getValueFromElsewhere(SomeOtherClass soc){
        return soc.blackBoxIntegerMachine();
    }
}

@isTest
private class MyClassTest{
    @isTest
    static void testHandlesException(){
        SomeOtherClass soc = new SomeOtherClass();
        soc.setThrowExceptionOnRun(true);

        MyClass mc = new MyClass();

        Boolean exceptionEncountered = false;

        try{
            Test.startTest();
            Integer result = mc.getValueFromElsewhere(soc);
            Test.stopTest();
        }catch(Exception e){
            exceptionEncountered = true;
        }

        system.assert(exceptionEncountered, 'Didn\'t encounter an exception');
    }
}

You can apply the idea of DI to other things, such as your query for Employee__c records. Instead of running your query directly, you can abstract that out into another class (which starts to look a lot like a Selector Layer class, i.e. the Selector pattern, from Apex Enterprise Patterns). You'd then inject that dependency into the class that uses it.

If done right, you could inject test data into the selector (to avoid the need to perform a query in the first place), and then inject the selector into the class you're testing.

public Class LazySelector{
    public Map<Id, SomeClass__c> cachedRecords = new Map<Id, SomeClass__c>();

    public List<SomeClass__c> getRecords(List<Id> targetIds){
        Set<Id> idsToQuery = new Set<Id>(targetIds);
        idsToQuery.removeAll(cachedRecords.keySet());

        // Only run the query if we have an Id that isn't cached already
        if(!idsToQuery.isEmpty()){
            cachedRecords.putAll([SELECT Id, <fields> FROM SomeClass__c WHERE Id IN :idsToQuery]);
        }

        return cachedRecords.clone().keySet().retainAll(targetIds);
    }
}

public class TriggerWorker{
    LazySelector selector;

    public TriggerWorker(LazySelector givenSelector){
        selector = givenSelector;
    }

    public void beforeUpdate(List<ChildClass__c> newList){
        // SomeClass__c is the parent of ChildClass__c
        // Fetch the parent records so we can do... something

        List<Id> someClassIds = new List<Id>();
        for(ChildClass__c child :newList){
            someClassIds.add(child.SomeClass__c);
        }

        Map<Id, SomeClass__c> parentMap = new Map<Id, SomeClass__c>(selector.getRecords(someClassIds));
    }
}

@isTest
private class TriggerWorkerTest{
    @isTest
    static void testGetsParents(){
        LazySelector testSelector = new LazySelector();

        SomeClass__c parent = new SomeClass__c(
            // We can make fake Ids so we don't need to actually do any inserting
            //   or querying
            // DML and Queries are two things that slow tests down
            Id = Schema.SObjectType.SomeClass__c.getKeyPrefix() + '1'.leftPad(12, '0'),
            Some_Field__c = 'value'
        );

        testSelector.cachedRecords.put(parent.Id, parent);

        ChildClass__c child = new ChildClass__c(
            SomeClass__c = parent.Id
        );

        TriggerWorker worker = new TriggerWorker(testSelector);
        Test.startTest();
        worker.beforeUpdate(new List<ChildClass__c>{child});
        Test.stopTest();
    }
}

You're not really removing complexity, merely shuffling it around

As with many engineering disciplines, it's about tradeoffs.

Your current code is pretty simple, and contained in a few, well-defined classes/triggers. Testing it, however, is a pain because you need to set up records for multiple objects and respect all of the validation/triggers they contain.

Moving to a trigger framework gives you more control and can make testing (and test setup) much easier. However, your code is now split among several different classes (and each should have their own test class). It'll likely be harder to design/write new code and take longer to develop as a result. At a certain point, it becomes difficult to bring on new resources to the code base.

Going full-in on Apex Enterprise Patterns and Dependency Injection can be overkill.

Moving to a trigger framework is pretty much a universal recommendation, but for the rest of it you'll need to decide if it's worth spending more time to design for DI (and how much) or if you can tolerate needing to jump through some extra hoops when setting up test data.

1

You can only skip those triggers if they have some built in functionalities to do so (e.g. built-in flags, switches to deactivate them or a trigger framework was used which supports it)

2
  • So I need to accommodate the needs of other more complex triggers, just to test this one simple trigger that is completely unrelated to the others? May 8, 2021 at 19:47
  • If the other triggers don't support this 'skip functionality', yes...
    – hajo77
    May 8, 2021 at 19:58

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .