0

I have installed NPSP in my organisation. I also created a custom object named as "Opportunity Group(OppGroup__c)" that will group my opportunities. What I want is that during deletion of an "Opportunity Group", all of its related opportunity and all NPSP allocation connected to those opportunities will be deleted. I've created the code below. Everything is working fine. But when I try to create a test class, I can't surpass 75% because I can't cover the else statement of the below code. Can you please help me how to create a test class for this ? Or do you think that what I did is a good practice ? If not what do you think is the better thing I need to do in regards with my code ?

trigger deleteOpportunityGroup on OppGroup__c (before delete) {
        List<Opportunity> oppList   = [SELECT Id from Opportunity where OppGroup__c IN :Trigger.oldMap.keySet()];
        List<npsp__Allocation__c> allocList = [SELECT Id from npsp__Allocation__c where npsp__Opportunity__c IN :oppList];

        if(allocList.size() < 10000)
            delete allocList;
        else {
            while(allocList.size() > 0) {
                integer count = 1;
                List<npsp__Allocation__c> tempList = new List<npsp__Allocation__c>();

                for(integer counter = 0 ; counter < allocList.size();) {
                    tempList.add(allocList[counter]);
                    allocList.remove(counter);
                    count++;
                    if(count == 10000)
                    break;
                }
                if(tempList.size() > 0)
                    delete tempList;
            }
        }

        if(oppList.size() < 10000)
            delete oppList;
        else {
            while(oppList.size() > 0) {
                integer count = 1;
                List<Opportunity> tempList = new List<Opportunity>();

                for(integer counter = 0 ; counter < oppList.size();) {
                    tempList.add(oppList[counter]);
                    oppList.remove(counter);
                    count++;
                    if(count == 10000)
                    break;
                }
                if(tempList.size() > 0)
                    delete tempList;
            }
        }
}
3
  • 1
    The only way you can cover the else part is by doing bulk testing 10K+ records. Or you can try Test.isRunningTest() but bulk testing is recommended. Commented Mar 16, 2018 at 7:15
  • @TusharSharma I've tried creating a 10k records in my test class but I encounter of different errors like , "Too many SOQL queries: 101" and "Too many DML rows: 10001"
    – Hope
    Commented Mar 16, 2018 at 7:22
  • Then I thing Test.isRunningTest() is only option for now. Commented Mar 16, 2018 at 7:23

2 Answers 2

1

You can't cause this condition to run in a test. Even if you could, you would hit the DML Row governor and the transaction will fail with a LimitException. That same limitation means this code will never run in a live transaction, so you can just remove the else block entirely.

1

Adrian makes a good point.

For sake of interest, however, let's pretend that instead of 10,000, your limit is something inside the realm of possibility, like 1,000.

With this more reasonable number, it is possible that you could create enough records in your test setup to be able to hit this limit. Your test will take longer to execute if you create a lot of test records, but it is posssible.

There is, however, another (arguably better) way to go about this...dependency injection

What is Dependency Injection?

In simple terms, it means that instead of creating values/objects that a given class or method depends on inside the class or method itself, e.g.

public class MyClass{
    public Integer myMethod(Integer otherNumber){
        Integer myDependency = 5;

        return otherNumber + myDependency;
    }
}

public class OtherClass{
    public void main(){
        MyClass mc = new MyClass();
        Integer result = mc.myMethod(4);
    }
}

you instead "inject" the dependency into the class/method/whatever from , e.g.

public class MyClass{
    private Integer myInjectedDependency;

    public MyClass(Integer injectedValue){
        this.myInjectedDependency = injectedValue;
    }

    public Integer myMethod(Integer otherNumber){
        return otherNumber + this.myInjectedDependency;
    }
}

public class OtherClass{
    public void main(){
        MyClass mc = new MyClass(5);
        Integer result = mc.myMethod(4);
    }
}

The important point is that while you depend on myInjectedDependency to have a value, that value is not directly set by the class that requires it (or rather that it can be set externally to the class that uses it). Instead, you push the responsibility of providing that value to the "client" of the class (i.e. the class or method that actually calls myMethod()).

There are different ways to accomplish dependency injection, the one I used in my example was "Constructor Injection".

How this helps with testing your trigger

The limit that you're using is 10,000, and is used in multiple places in your trigger. That makes it a good candidate to become a variable.

If you could inject a value into this variable for some of your test methods, you could make it a whole lot easier to test your code. Instead of setting the limit to 10,000, you could set it much, much lower.

Injecting a value of 3 for this limit would keep your test setup and execution fast, and you would still be able to test all of the important cases:

  • If you only have one record, it's guaranteed that you will execute the "if" branch of your code
  • If you have two records, you test that one of the boundary conditions of your "if" condition is correct and that you still execute the "if" branch
  • If you have three records, you test another boundary condition for your "if" condition, and that you execute the "else" branch
  • If you have four records, you test the final boundary condition, will enter your "else" branch, and you will execute two delete DML statements

How to implement

Since this logic is inside a trigger, our options are a bit more limited. Triggers don't have constructors, we can't pass additional paramaters to a trigger in the same way that we would a method in a class, and declaring static variables inside of a trigger is completely pointless (as the "static" value is reset for each chunk of 200 records a trigger processes as well as when the records within a common trigger chunk transition between trigger contexts like "before update" and "after update").

Best practice is to keep triggers "logicless". That means, at the very least, that you would take your current trigger code, move it to its own Apex class, and then instantiate and call your class from within the trigger). In practice, this is when most people start to use a trigger framework.

If you're not ready to make that jump quite yet, you can still make use of dependency injection by creating an Apex class with just one static variable

public class OppGroupDeleteLimit{
    // Setting a default value is fine
    // The important part is that we can change this limit in your test class
    // Using a public static variable here is not best practice, I'll explain
    //   more in a footnote
    public static Integer limit = 1000;
}

You would then modify your trigger to use this static variable

trigger deleteOpportunityGroup on OppGroup__c (before delete) {
        List<Opportunity> oppList   = [SELECT Id from Opportunity where OppGroup__c IN :Trigger.oldMap.keySet()];
        List<npsp__Allocation__c> allocList = [SELECT Id from npsp__Allocation__c where npsp__Opportunity__c IN :oppList];

        if(allocList.size() < OppGroupDeleteLimit.limit /*10000*/)
            delete allocList;
        else {
            while(allocList.size() > 0) {
                integer count = 1;
                List<npsp__Allocation__c> tempList = new List<npsp__Allocation__c>();

                for(integer counter = 0 ; counter < allocList.size();) {
                    tempList.add(allocList[counter]);
                    allocList.remove(counter);
                    count++;
                    if(count == OppGroupDeleteLimit.limit /*10000*/)
                    break;
                }
                if(tempList.size() > 0)
                    delete tempList;
            }
        }

        // Rest of code omitted, but I hope you get the picture
}

By doing this, you give yourself the ability to inject whatever limiting value you want into your trigger. In your test method, all you'd need to do is OppGroupDeleteLimit.limit = 4;

The gotcha

As is, your trigger will not behave exactly as you want it to. The governor limit for DML rows is 10,000, and that applies to the entire transaction (that limit is not reset after one DML statement finishes executing).

If you delete 1500 OppGroup__c records, then that will leave you with (at most) 8500 other records that you can delete. If those 1500 OppGroup__c records are related to 10,000 Opportunities, your trigger will fail. If you have other triggers that are run on Opportunity or npsp__Allocation__c, then those triggers also count towards your governor limits. If you start execution of a class/trigger "A", all of the classes/triggers that are called as a result of executing "A" are in the same transaction as "A".

If you truly do need to support deleting such a large amount of records, you should be looking at an asynchronous solution (like batch apex, queueable, or @future).

Footnotes:

Generally speaking, it's good object-oriented practice to restrict access to class variables as much as possible (make everything private, if possible).

One of the reasons to do this is so that you can better control who/what can read and write to a variable.

In the example I gave above, being public, anyone could change that static variable to anything they wished. This can lead to some issues that are hard to debug (what do you mean I've used 100 DML statements?, I only delete 200 records!).

Better practice would be something along the lines of

public class OppGroupDeleteLimit{
    @testVisible
    private static Integer limit = 1000;

    public static Integer getLimit(){
        return limit;
    }
}
  • Making the static variable private means nothing outside of this class can read or write the value (excellent control)
  • That's a bit excessive though, so we provide a public (and static) method that allows anyone to read the value
  • We annotate the static Integer with @testVisible to allow us to read/write the value of this field directly as part of a test method (and nowhere else)
2
  • 2
    N.B. great post; I'd use @TestVisible static Integer limit = 1000; rather than public
    – cropredy
    Commented Mar 16, 2018 at 17:20
  • @cropredy Completely agree. I just got lazy and didn't want to explain why that was better practice than just using a public static. I suppose I can add that in as a footnote before I go to lunch.
    – Derek F
    Commented Mar 16, 2018 at 17:29

You must log in to answer this question.

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