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)
Test.isRunningTest()
but bulk testing is recommended.Test.isRunningTest()
is only option for now.