Clean the mess!
For start let's define some "more-or-less" real life scenario for those, who want to follow in their orgs. We have a custom object Log (API Name: Log__c, Name Type: Auto Number), that we use to log exceptions, integrations and some other events worth saving. Custom object has several fields containing information like error message and stack trace, but it is not really important for our purpose. Unfortunately our predecessors (or anyone else we can easily blame) didn’t do their work properly creating immense number of bugs in the system and our object is overloaded with logged exceptions and takes too much of the storage.
Fixing all the bugs in the system will take months, so we need to take care of the storage first. Our 2 000 hard-working Oompa Loompas… 2 000 hard working users are daily generating more records, than we can delete in one transaction. We also need to store logs for at least 7 days. Let’s quickly write some Batch Apex, that will be run every night out of working hours.
The code is pretty clear, so I will not spend any more time commenting it.
global class
DeleteLogsimplements
Schedulable, Database.Batchable<SObject> {private
DateTime DELETION_DEADLINE;global
Database.QueryLocator start (Database.BatchableContext BC) { DELETION_DEADLINE = DateTime.now().addDays(-6);return
Database.getQueryLocator('SELECT Id FROM Log__c WHERE CreatedDate < :DELETION_DEADLINE'
); }global void
execute(Database.BatchableContext BC,List
<Log__c> scope) {delete
scope; }global void
finish(Database.BatchableContext BC) { }global void
execute(SchedulableContext SC) { Database.executeBatch(new DeleteLogs()); } }
Alright, although the class is a bit ugly, it should work. We are going to celebrate it with 100 % code coverage. First we will try to do it in one method. We will schedule our batch inbetween Test.startTest() and Test.stopTest().
@IsTestprivate class
DeleteLogsTest {private static final
Integer TOTAL_RECORDS = 3;private static final
Integer RECORDS_TO_DELETE = 2;private static final
Integer REMAINING_RECORDS = TOTAL_RECORDS - RECORDS_TO_DELETE; // Let's create Log records, but only some of them older than 7 days @testSetupstatic void
setup() { // Create logsList
<Log__c> logs = newList
<Log__c>();for
(Integer i = 0; i < TOTAL_RECORDS; i++) { logs.add(new Log__c()); }insert
logs; // Set createdDate 7 days back for some of the recordsfor
(Integer i = 0; i < RECORDS_TO_DELETE; i++) { // "Midnights tests" won't cause any issue here, because we are not seeking // records exactly 7 days old, but at least 7 days oldTest
.setCreatedDate(logs[i].Id, DateTime.now().addDays(-7)); } } // Let's test both Batchable and Schedulable at once @IsTestprivate static void
testWillNotWork() {Test
.StartTest();System
.schedule('Log Deletion'
,'0 0 1 * * ?'
, new DeleteLogs());Test
.StopTest();List
<Log__c> remainingLogs = [SELECT
IdFROM
Log__c];System
.assertEquals(REMAINING_RECORDS, remainingLogs.size
(),'Incorrect number of records was deleted.'
); } }
Proper test
Assertation failed, because no record was deleted. But why, when documentation says "All asynchronous calls made after the startTest method are collected by the system. When stopTest is executed, all asynchronous processes are run synchronously."? Because system doesn't collect asynchronous processes nested in other asynchronous processes. It works on one level only.
So, how can we achieve our goal of 100 % code coverage? We need to test Batchable and Schedulable part separately. In testBatchable method we are going to test deletion of records and in testSchedulable we are only testing, if Schedulable part executes Batchable.
@IsTestprivate class
DeleteLogsTest {private static final
Integer TOTAL_RECORDS = 3;private static final
Integer RECORDS_TO_DELETE = 2;private static final
Integer REMAINING_RECORDS = TOTAL_RECORDS - RECORDS_TO_DELETE; // Let's create Log records, but only some of them older than 7 days @testSetupprivate static void
setup() { // Create logsList
<Log__c> logs = newList
<Log__c>();for
(Integer i = 0; i < TOTAL_RECORDS; i++) { logs.add(new Log__c()); }insert
logs; // Set createdDate 7 days back for some of the recordsfor
(Integer i = 0; i < RECORDS_TO_DELETE; i++) { // "Midnights tests" won't cause any issue here, because we are not seeking // records exactly 7 days old, but at least 7 days oldTest
.setCreatedDate(logs[i].Id, DateTime.now().addDays(-7)); } } @IsTeststatic void
testBatchable() {Test
.StartTest(); Database.executeBatch(new DeleteLogs());Test
.StopTest();List
<Log__c> remainingLogs = [SELECT
IdFROM
Log__c];System
.assertEquals(REMAINING_RECORDS, remainingLogs.size
(),'Incorrect number of records was deleted.'
); } @IsTestprivate static void
testSchedulable() {Test
.startTest();System
.schedule('Delete Logs Schedulable'
,'0 0 1 * * ?'
, new DeleteLogs());Test
.stopTest();List
<AsyncApexJob> batchJobs = [SELECT
Id, StatusFROM
AsyncApexJobWHERE
ApexClass.Name ='DeleteLogs'
AND
JobType ='BatchApex'
];System
.assertEquals(1, batchJobs.size
(),'Unexpected number of batch jobs ran: '
+ batchJobs);System
.assertEquals('Queued'
, batchJobs[0].Status,'Job planned with an unexpected status.'
); } }
Run Tests… 1, 2… Yes! We did it again!
Looking for an experienced Salesforce Architect?
- Are you considering Salesforce for your company but unsure of where to begin?
- Planning a Salesforce implementation and in need of seasoned expertise?
- Already using Salesforce but not fully satisfied with the outcome?
- Facing roadblocks in your Salesforce implementation and require assistance to progress?
Feel free to review my profile and reach out for tailored support to meet your needs!
Comments
Post a Comment