I have a fresh case of strange Apex behavior that is throwing me for an absolute loop.
I have a trigger on after insert
and after update
that needs to cycle through some records. It will update those that are determined to be valid, and toss out those that aren't by tagging them with addError(String)
. It is written to do this in a for loop but in practice (and this unit test) the trigger never gets invoked with an actual List of records, just a single one.
The weird part is, debug logs being enabled or disabled seems to be directly changing how this trigger will work when tested.
When logs are enabled, all is fine. The test runs, invokes the trigger, addError(String)
is called, the test catches the exception and asserts true.
When logs are disabled, which is super convenient because I am now blind to what is executed, the test runs, I think the trigger runs, but then the test hits its System.assert(false)
, a line I added to detect when, just like this, the exception was expected but didn't happen.
I first noticed this behavior after successfully testing, with debug logs enabled, in VSCode with the SFDX extensions and Salesforce CLI. After uploading it as a change set and running a validation, that is when I saw the tests fail, and when I noticed that they began failing in VSCode/SFDX extensions conveniently after the timer on my debug logs ran out, I tried a few more runs with and without logs to confirm this was the variable in question.
Of course none of this means anything without some code to reference; obfuscated for privacy and snipped for relevance but here is what I am working with:
Apex Trigger
final List<Id> RECORD_TYPE_IDS = new Id[]
{
'0123456789abcdef01', '0123456789abcdef02' // and more
};
Map<Id, Account> accountMap = new Map<Id, Account>([SELECT Id FROM Account]);
for (Custom_Object__c customRecord : (List<Custom_Object__c>) Trigger.New)
{
Account acc = accountMap.get(customRecord.Account_Name__c);
if (RECORD_TYPE_IDS.contains(customRecord.RecordTypeId))
{
// Note: Year__c is actually stored as a String, not a number.
List<Custom_Object__c> otherCustomRecords =
[
SELECT Year__c, RecordTypeId
FROM Custom_Object__c
WHERE Id != :customRecord.Id
AND Account_Name__c = :acc.Id
AND Year__c LIKE :'%' + customRecord.Year__c + '%'
AND RecordTypeId IN :RECORD_TYPE_IDS
];
if (otherCustomRecords.size() > 0)
{
customRecord.Year__c.addError('Custom Record already exists for the year ' + customRecord.Year__c);
}
}
}
Test Class
@TestSetup
static void setup()
{
Account acct = new Account(Name = 'Test Account');
insert acct;
// The first record for a year
Custom_Object__c testRecord = new Custom_Object__c
(
RecordTypeId = '0123456789abcdef01',
Account_Name__c = acct.Id,
Year__c = '1999'
);
insert testRecord;
}
@IsTest
static void test()
{
Test.startTest();
Account acct = [SELECT Id FROM Account LIMIT 1];
try
{
Custom_Object__c customRecord = new Custom_Object__c
(
RecordTypeId = '0123456789abcdef01',
Account_Name__c = acct.Id,
Year__c = '1999'
);
insert customRecord;
System.assert(false);
}
catch (Exception e)
{
// addError(String) should cause an Exception to be thrown
System.assert(true);
}
Test.stopTest();
}
So, details established, the big two questions:
Why on Earth would enabling debug logs affect how this test is behaving like this? Am I on the right track that it's something up with the
addError(String)
method or is the problem outside of that?How do I go about fixing this, and what are some things to look for in the future to prevent this?
Additional detail: Moving Test.startTest()
and Test.stopTest()
around so that the trigger invocation was not inside them had no effect.
Updates/Attempts: Anything that I have tried from recommendations here
- Attempted to swap from after trigger to before trigger, since addError(String) is intended for before triggers. Appeared at first to fix it, but instead was just giving a new fail reason because the trigger was not properly converted from after to before yet. After properly adjusting field assignments and removing update statements, the old behavior (no debug logs = fail assertion, debug logs = pass assertion) returned.