Have you written try catch in your helper classes, to capture expected errors and handled it ? Then you will also agree the exception handling is a repeated piece of code every class has. It is a pain in back to cover in test class. It becomes a nightmare if the class has less lines, and you are stuck with the uncovered catch blocks. So, today I am sharing an approach that I tried.
Is a and Has a
Is a and Has a are the concepts from Object Oriented programming. Is a is from Inheritance, Has a is from composition.
I will not discuss much about it, but we are going to use the Is a concept here.
Logger Class
Most projects has their implementation of logger. I am going to use the logger class that I created in the custom settings to switch your logs on and off. Basically I moved the code execution and try catch into the logger class itself. When I want to execute some method which might throw exception, I call my logger to watch it and handle any exception that may occur.
public static Object watch( String className, String methodName, Map<String, Object> args ) { Loggable loggable = (Loggable) Type.forName(className) .newInstance(); try { return loggable.call(methodName, args); } catch (Exception exc) { Logger logger = new Logger(className); logger.error(exc.getMessage()); return exc; } }
The watch method accepts three parameters. Name of the class, name of the method and key-value pair of arguments that I need to pass into the method execution. I am using the Type class to dynamically create an instance of the class which I want to execute. The classes which can be called by logger has to implement the Loggable interface. I will talk more about it later in the post. But basically it has the call method which is implemented by the class I want to call.
An example class implementing Loggablepublic with sharing class AccountService implements Loggable { public Account getAccountByAccountNumber(String accountNumber) { return [ SELECT Id, AccountNumber Name FROM Account WHERE AccountNumber = :accountNumber WITH SECURITY_ENFORCED ]; } // bad code(query without limits and security) public Account getAllAccounts() { return [SELECT Id, AccountNumber Name FROM Account]; }
public Object call(String methodName, Map<String, Object> args) { switch on methodName { when 'getAccountByAccountNumber' { return this.getAccountByAccountNumber( (String) args.get('accountNumber') ); } when 'getAllAccounts' { return this.getAllAccounts(); } when else { return null; } } }}
Basically when a method name is passed as string, I am calling the actual method and passing arguments from the named arguments map after typecasting. I got the idea while I was working on a Queueable adding itself to the queue to do a different task. I am using the same concept there as well except for the arguments.
If you are a JavaScript developer like me, you can relate this easily as everything in JavaScript is an object.
Finally the Loggable interface. Loggable is a copy of apex Callable interface, though I did not inherit from it just to make sure, it is only used in logger context.
public interface Loggable { Object call(String action, Map<String, Object> argsByName);}
Now that I have everything setup, lets see an how to call logger to watch a method.
@IsTestprivate class AccountService_Test { @IsTest static void getAccountByAccountNumberTest() { insert new Account(Name = 'Test Company', AccountNumber = '12345'); Map<String, String> params = new Map<String, String>(); params.put('accountNumber', '12345'); Test.startTest(); Account account = (Account) Logger.watch( 'AccountService', 'getAccountByAccountNumber', params ); Test.stopTest(); System.assertNotEquals( null, account, 'Expected an account to be returned for the given account number' ); System.assertEquals( '12345', account.AccountNumber, 'Expected the account number to match "12345"' ); }}
Let me know if this was helpful. Share your suggestions and comments.
public static Object watch(
String className,
String methodName,
Map<String, Object> args
) {
Loggable loggable = (Loggable) Type.forName(className)
.newInstance();
try {
return loggable.call(methodName, args);
} catch (Exception exc) {
Logger logger = new Logger(className);
logger.error(exc.getMessage());
return exc;
}
}
An example class implementing Loggable
public with sharing class AccountService implements Loggable {
public Account getAccountByAccountNumber(String accountNumber) {
return [
SELECT Id, AccountNumber Name
FROM Account
WHERE AccountNumber = :accountNumber
WITH SECURITY_ENFORCED
];
}
// bad code(query without limits and security)
public Account getAllAccounts() {
return [SELECT Id, AccountNumber Name FROM Account];
}
public Object call(String methodName, Map<String, Object> args) {
switch on methodName {
when 'getAccountByAccountNumber' {
return this.getAccountByAccountNumber(
(String) args.get('accountNumber')
);
}
when 'getAllAccounts' {
return this.getAllAccounts();
}
when else {
return null;
}
}
}
}
public interface Loggable {
Object call(String action, Map<String, Object> argsByName);
}
@IsTest
private class AccountService_Test {
@IsTest
static void getAccountByAccountNumberTest() {
insert new Account(Name = 'Test Company',
AccountNumber = '12345');
Map<String, String> params = new Map<String, String>();
params.put('accountNumber', '12345');
Test.startTest();
Account account = (Account) Logger.watch(
'AccountService',
'getAccountByAccountNumber',
params
);
Test.stopTest();
System.assertNotEquals(
null,
account,
'Expected an account to be returned for
the given account number'
);
System.assertEquals(
'12345',
account.AccountNumber,
'Expected the account number to match "12345"'
);
}
}
0 Comments