4

Background: Recently I wrote a very simple Batch to run on the 1st of every month. Its purpose is to query for any Project__c records that aren't Complete, and create a related "Carry-Over" record in our tracking/reporting object.

To make the batch more efficient, I added AND Owner.Type = 'User' to my database.queryLocator to avoid batching through items owned by a queue.


Issue: I then wrote a test class to run the batch and verify the outcome, but the batch never ran correctly.

A few google searches later, I discovered this Known Issue reported back in Feb 2017 that has been marked as "NO FIX" by Salesforce: https://success.salesforce.com/issues_view?id=a1p30000000SwHDAA0

The readers digest version is that apparently Owner fields (like Owner.Type) are not retrieved via SOQL on custom objects in Test Classes.

So, since its not retrieved by SOQL in test classes, my batch running in test context is seeing this value as NULL as well, so the database.queryLocator returns no results, the batch has no records to process, and the post-batch asserts fail.


Already Tried: I tried changing my SOQL query from AND Owner.Type = 'User' to AND (NOT OwnerId LIKE '00G%), but the LIKE operator only works on Strings, unfortunately.


Ugly Solution: I could remove the Owner.Type portion of my SOQL query entirely, but the resulting list would include more records than intended, and I'd just have to add logic to my execute method to compare the SObjectType of the OwnerId field of each record to the User.SObjectType and throw out any records that don't match the SObjectType, but that makes my batch less efficient and potentially adds many unnecessary batches.


Question: Other than my "Ugly Solution" above, is there any way that I can manipulate this data in a way that would let the batch see Owner.Type in test context? Or am I stuck with deciding between an untestable (but efficient) batch, a testable (but grossly inefficient) batch, or swapping Owner.Type with a custom boolean field to show true/false based on Owner Type (presuming this option would even work any differently)?

Additionally, can anyone provide any insight into WHY this odd limitation in Test Classes exists, and why Salesforce may have deemed it as "Non-Fixable"?


Code for Reference:

Batch Class:

global class Project_CarryOver_Batch implements Database.Batchable<sObject> {
    private static final Map<String, Schema.RecordTypeInfo> projectRTs = Schema.SObjectType.Project__c.getRecordTypeInfosByName();
    private static final String CARRY_OVER = 'Carry-Over';
    private static final String PROJECT_DEBUG = 'DEBUG';
    private static final String BATCH_COMPLETE = 'BATCH COMPLETE';
    private static final String ERROR_EXCEPTION = 'ERROR';
    global Project_CarryOver_Batch() {

    }

    global Database.QueryLocator start(Database.BatchableContext BC) {

        Id projectRT = projectRTs.get(MasterProjectTrigHdlr.PROJECT_TEMPLATE).getRecordTypeId();
        return Database.getQueryLocator([SELECT Id,OwnerId 
                                        FROM Project__c 
                                        WHERE Status__c != :MasterProjectTrigHdlr.ABANDONED 
                                        AND Status__c != :MasterProjectTrigHdlr.COMPLETE 
                                        AND RecordTypeId != :projectRT 
                                        AND Owner.Type = 'User' 
                                        AND Id NOT IN (SELECT Project__c FROM Customer_Journey__c WHERE CreatedDate = THIS_MONTH)
                                        ]);
    }

    global void execute(Database.BatchableContext BC, List<Project__c> scope) {
        List<Log__c> logs = new List<Log__c>();
        List<Customer_Journey__c> carryOvers = new List<Customer_Journey__c>();
        logs.add(LogUtil.createLog(PROJECT_DEBUG,'Scope: ' + JSON.serialize(scope),'Project_CarryOver_Batch','execute'));
        if (!scope.isEmpty()) {
            for(Project__c p : scope){
                Customer_Journey__c j = new Customer_Journey__c(Date__c = System.now(),
                                                                OwnerId = p.OwnerId,
                                                                Project__c = p.Id,
                                                                Type__c = CARRY_OVER);
                carryOvers.add(j);
            }
        }

        if(!carryOvers.isEmpty()){
            List<Database.SaveResult> results = Database.insert(carryOvers, false);
            Integer i = 0;
            for(Database.SaveResult result : results){
                String errorString = '';
                Integer errorCount = 0;
                if(!result.isSuccess()){
                    for(Database.Error err : result.getErrors()){
                        errorCount++;
                        errorString += 'PROJECT ID -> ' + carryOvers[i].Project__c + ' : Error #' + errorCount + ' : Status Code-> ' + err.getStatusCode() + ' : Error Message -> ' + err.getMessage() + ' : Fields -> ' + err.getFields();
                        logs.add(LogUtil.createLog(ERROR_EXCEPTION,errorString,'Project_CarryOver_Batch','execute'));
                    }
                }
                i++;
            }
        }

        if(!logs.isEmpty()){
            LogUtil.saveLogs(logs);
        }
    }

    global void finish(Database.BatchableContext BC) {
        LogUtil.saveLogs(new List<Log__c>{LogUtil.createLog(BATCH_COMPLETE,'The Batch Job Finished at: ' + System.now(),'Project_CarryOver_Batch','finish')});
    }
}

Test Method:

@isTest
    private static void CarryOver_Batch_TEST() {
        List<Customer_Journey__c> preJourneys = [SELECT Id 
                                                FROM Customer_Journey__c 
                                                WHERE Type__c = :MasterProjectTrigHdlr.CARRY_OVER];
        List<Project__c> preProj = [SELECT Id,OwnerId,Owner.FirstName,Owner.Type 
                                    FROM Project__c WHERE RecordTypeId = :csProjectRT AND Project_Template__r.Name = 'Test Onboarding Template'];

        System.assertEquals(5,preProj.size());
        System.assertEquals(0,preJourneys.size());
        Test.startTest();
        Project_CarryOver_Batch carryOver = new Project_CarryOver_Batch();
        Database.executeBatch(carryOver);
        Test.stopTest();

        List<Customer_Journey__c> postJourneys = [SELECT Id 
                                                FROM Customer_Journey__c 
                                                WHERE Type__c = :MasterProjectTrigHdlr.CARRY_OVER];

        System.assertEquals(3,postJourneys.size()); // FAILS BECAUSE BATCH WAS NULL DUE TO Owner.Type
    }
  • did you try a custom field on your custom object (formula) that is called Is_Owned_by_User__c and filter on that field in the start() method? – cropredy Dec 19 '19 at 23:36
  • That's one of the approaches I considered above, but wasn't sure if there was a more "correct" way to do this without leveraging custom fields. We plan on adding similar batches for cases, oops, etc so I just didn't like the idea of making an extra field unless absolutely necessary. Plus I assume that the formula field also won't see Owner.Type in test context so I'd have to do a string comparison on 00G which presumes that queue prefixes will never change. It will work, but is it best? – Morgan Marchese Dec 19 '19 at 23:58
  • a) the KI is about custom objects so WHERE Owner.Type = .. should work fine on standard objects like Case or Opportunity; b) the custom formula field does work in a testmethod (I just tried); c) a more exotic option is to use selector classes and mock the result of the selector; testing the start(), execute(), and finish() separately – cropredy Dec 20 '19 at 1:40
  • @cropredy fyi, I just started building the test class for the Case object batch class, and Owner.Type is null in test classes when working with Cases as well. Seems that this issue exists on standard objects too, necessitating a custom formula (checkbox) field on all objects that try to leverage that related data. – Morgan Marchese Dec 21 '19 at 13:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.