We use the object oriented approach of using non-static methods because of the massive benefits we can derive in unit testing (and potentially also in production cases, though we don't do that yet).
We take a "dependency injection factory method" or "dependency injection singleton access" approach. Each of our trigger handlers provides an interface or set of virtual methods, meaning we can provide handlers that vary the behaviour. When the trigger wants to access an instance it uses a factory method (if parameters are required for the object initialization) or a singleton access method. Here I'll provide the singleton example for simplicity.
The factory method/singleton access is itself static. Behind it we leverage a Type. The Type is held in a private static value. Let's start with an example:
public virtual with sharing class MyHandler {
private static Type instanceType = MyHandler.class;
private static MyHandler instance = null;
public static Type setInstanceType(Type instanceType) {
Type originalType = MyHandler.instanceType;
if (instanceType != null) {
MyHandler.instanceType = instanceType;
instance = null;
}
return originalType;
}
public static MyHandler getInstance() {
if (instance == null) {
instance = (MyHandler) instanceType.newInstance();
}
return instance;
}
public virtual void handleInsert(List<MyObject__c> newObjects) {
...;
}
}
Something to note is the setInstanceType method. This lets us change the implementation used whenever we want to, while instanceType defaults to the actual "production" type so we don't have to call this method normally.
The trigger can then do:
trigger MyObject on MyObject__c (before insert) {
MyHandler handler = MyHandler.getInstance();
handler.handleInsert(Trigger.new);
}
We can nicely unit test the actual handler itself, outside the context of the trigger, by doing something like:
@IsTest
class TestMyHandler {
static Integer handleInsertCalledCount = 0;
class NoOpHandler extends MyHandler {
public override void handleInsert(List<MyObject__c> newObjects) {
handleInsertCalledCount++;
}
}
@IsTest
static void testInsert() {
// Turn off all processing in the trigger
MyHandler.setInstanceType(NoOpHandler.class);
MyHandler handlerUnderTest = new MyHandler();
// Do whatever is needed to test the handler handleInsert method. Even if
// we have to insert MyObject__c instances we know the handler isn't
// actually called - instead the "no-op" version is used. Since we have a
// counter that is incremented we can check that the handler is called the
// expected number of times when the trigger does invoke it, for example
}
}
This allows for appropriate and true unit testing of code even when actual object inserts/updates/deletes are performed - and all of the latter can have their processing turned off. It is also easy to write variants that throw exceptions or add errors to objects to simulate error cases in order to test behaviours of classes that themselves manage instances of the target object type (MyObject__c here) through DML operations.
This also allows for specific code flows that explicitly disable the trigger processing before DML operations, re-enabling it afterwards like:
MyHandler original = MyHandler.setInstanceType(ProductionNoOpHandler.class);
try {
insert myObjects;
} finally {
MyHandler.setInstanceType(original);
}
Where we want to support handler initialization parameters, this can be done using real parameters to the (e.g.) newInstance factory method, where these parameters get turned into an appropriate JSON string that allows us to then use JSON.deserialize to the target type, but that's just a slight variation on this theme.
Fundamentally the fewer static methods you have in your code base the more adaptable, better object-oriented and more properly unit-testable your code truly is. And that's not just in the case of trigger handlers but across the board.
UPDATE: I did forget to mention the Apex Stub API "mocking capability", what little there is of it, can be applied in this pattern too - we have used it a bit though only to support primitive aspects of testing such as invocation counting and method invocation sequence testing.
TriggerHandler
base class (e.g. github.com/kevinohara80/sfdc-trigger-framework) and have your individual trigger handlers inherit from it, but that's been answered in so many places I don't know if you've already seen that pattern and dismissed it.