The Salesforce Winter ’25 release introduces several exciting updates for developers, designed to boost performance and make development easier. In this overview, the ENWAY team highlights the most important updates that will help you get the most out of Winter ’25.
Mock SOQL Tests for External Objects Is Now Generally Available
From the Winter ’25 release, you can improve code coverage and quality by creating better Apex unit tests for external objects. Use new SOQL stub methods and a test class to mock SOQL query responses, allowing basic and joined queries to return mock data in your tests.
Extend the System.SoqlStubProvider class and override the handleSoqlQuery() method to create mock test classes. Use Test.createStubQueryRow() or Test.createStubQueryRows() to generate mock external object records and register the mock provider using Test.createSoqlStub().
Be careful: Apex governor limits still apply. The SOQL query must involve an external object directly or via a subquery. The following features are not supported within a stub implementation:
- SOQL
- SOSL
- Callouts
- Future methods
- Queueable jobs
- Batch jobs
- DML
- Platform events
The example below shows how to use the System.SoqlStubProvider class in combination with the createSoqlStub() and createStubQueryRow() methods in a unit test.
/**
* Utility class that runs queries to be mocked
* in the Apex tests.
**/
public class QueryIssueUtil {
public boolean queryGithubIssuesAndCheckForId() {
// BINDS WITH USER_MODE DYNAMIC QUERY
Map<String, Object> binds = new Map<String, Object>{
'tmpVar1' => 'x08xx000002HNZ6AAO'
};
List<GithubIssues__x> issues = Database.queryWithBinds(
'SELECT Id FROM GithubIssues__x WHERE Id = :tmpVar1',
binds,
AccessLevel.USER_MODE
);
for (GithubIssues__x issue : issues) {
if (issue.Id.equals('x08xx000002HNZ6AAO')) {
return true;
}
}
return false;
}
public SObjectType getSObjectTypeForDynamicSoql(String name) {
Schema.DescribeSObjectResult[] descResult = Schema.describeSobjects(
new List<String>{name}
);
SObjectType type = descResult.get(0).getSobjectType();
return type;
}
}
/**
* Test class that utilizes the SoqlStubProvider classes.
* Each test sets the appropriate SoqlStubProvider
* and runs validation against the mocked query results.
**/
@isTest
public class GithubIssueTest {
@isTest
static void testGithubIssueQuery() {
QueryIssueUtil queryIssueUtil = new QueryIssueUtil();
SObjectType type = queryIssueUtil.getSObjectTypeForDynamicSoql('GithubIssues__x’);
Test.createSoqlStub(type, new IssueStubProvider());
Test.startTest();
Assert.isTrue(Test.isSoqlStubDefined(type));
Assert.isTrue(queryIssueUtil.queryGithubIssuesAndCheckForId());
Assert.areEqual(Limits.getQueries(), 1);
Assert.areEqual(Limits.getQueryRows(), 1);
Assert.areEqual(Limits.getAggregateQueries(), 0);
Test.stopTest();
}
}/**
* Test class that utilizes the SoqlStubProvider classes.
* Each test sets the appropriate SoqlStubProvider
* and runs validation against the mocked query results.
**/
@isTest
public class GithubIssueTest {
@isTest
static void testGithubIssueQuery() {
QueryIssueUtil queryIssueUtil = new QueryIssueUtil();
SObjectType type = queryIssueUtil.getSObjectTypeForDynamicSoql(
'GithubIssues__x'
);
Test.createSoqlStub(type, new IssueStubProvider());
Test.startTest();
Assert.isTrue(Test.isSoqlStubDefined(type));
Assert.isTrue(queryIssueUtil.queryGithubIssuesAndCheckForId());
Assert.areEqual(Limits.getQueries(), 1);
Assert.areEqual(Limits.getQueryRows(), 1);
Assert.areEqual(Limits.getAggregateQueries(), 0);
Test.stopTest();
}
}
/**
* SoqlStubProvider class that returns a mocked query result
* for queries against the Github Issues object.
**/
public class IssueStubProvider extends SoqlStubProvider {
public override List<SObject> handleSoqlQuery(
SObjectType sobjectType,
String rawQuery,
Map<String, Object> binds
) {
if (sobjectType.equals(GithubIssues__x.SObjectType)) {
Assert.areEqual(binds.size(), 1);
Assert.areEqual(binds.get('tmpVar1'), 'x08xx000002HNZ6AAO');
List<SObject> objs = new List<SObject>();
Map<String, Object> individualMap = new Map<String, Object>{
'Id' => 'x08xx000002HNZ6AAO'
};
GithubIssues__x obj = (GithubIssues__x) Test.createStubQueryRow(
sobjectType, individualMap
);
objs.add(obj);
return objs;
}
return null;
}
}
More Consistent Set Iteration
Starting with API version 62.0, modifying a set’s elements during iteration using a for or foreach() loop will cause an error. In versions 61.0 and earlier, changing a set during a loop was sometimes allowed, but could lead to problems. This update helps keep things predictable.
This is the execution log for API version 61.0:
In API version 62.0 and later, you’ll see an error: System.FinalException: Cannot modify a collection while it is being iterated.
LWC API 62.0
Access to the Component’s Style Information
In LWC API version 62.0 and later, you can access a component’s host CSSStyleDeclaration object using this.style. This allows you to easily change the component’s style at runtime.
renderedCallback() {
this.style.color = 'red';
}
You can also use methods from the CSSStyleDeclaration class with this.style.
import { LightningElement } from "lwc";
export default class extends LightningElement {
static renderMode = "light"; // Default is 'shadow'
setStyle() {
this.style.setProperty('color', 'red');
this.style.setProperty('border', '1px solid #eee');
console.log(this.style.color); // Logs "red"
}
}
In LWC API version 61.0 and earlier, this.style would return undefined in light DOM. You could use this.children[0].parentElement.style as an alternative. In shadow DOM, you can use this.template.host.style and this.style interchangeably.
Class Object Binding for Managing Styles
Starting with LWC API version 62.0, you can now assign multiple classes to an element using a JavaScript array or object. This removes the need to concatenate strings for applying multiple classes.
Instead of manually combining class names, pass an array or object to the class attribute. For example, if a button’s style depends on different properties, use class object binding to dynamically assign classes.
<button onclick={doSomething} class={computedClassNames}>Submit</button>
import { LightningElement } from 'lwc';
export default class extends LightningElement {
variant = null;
position = "left";
fullWidth = true;
disabled = false;
// Class binding with an object
get computedClassNames() {
return [
"button__icon",
this.variant && `button_${this.variant}`,
this.position && `button_${this.position}`,
{
"button_full-width": this.fullWidth,
"button_disabled": this.disabled,
},
];
}
}
In LWC API version 62.0, the element renders correctly with all classes:
<button class="button__icon button_left button_full-width">Submit</button>
In earlier versions (61.0 and below), the output may render incorrectly:
<button class="button__icon,,button_left,[object Object]">Submit</button>
Parallel Subscriptions for Apex Triggers
To improve platform event processing in Apex triggers, use parallel subscriptions to process multiple events at once, rather than in a single stream. This allows your Apex triggers to handle large volumes of custom high-volume platform events more efficiently. Parallel subscriptions are not available for standard or change events.
Events are distributed across parallel subscriptions based on the partition key you choose, either the standard EventUuid field or a custom platform event field. You can set up to 10 parallel subscriptions, also called partitions.
Source: salesforce.com
The partition key you choose affects whether you prioritize performance or event processing order for your triggers.
For Best Performance: If event order doesn’t matter, select a partition key with a wide range of unique values, like IDs. This ensures events are evenly distributed across subscriptions based on the partition key’s hash value. Good options include the EventUuid field or a custom field with ID values.
To Preserve Event Order: If you need to maintain the order of events with the same key, use a partition key with fixed values, like categories or regions. Events with the same key are processed in their original order within the same partition.
To configure parallel subscriptions for an Apex trigger, use the Tooling API or Metadata API to define the event field for partitioning (PartitionKey) and the number of partitions (NumPartitions) in the PlatformEventSubscriberConfig.
{
"DeveloperName": "MyOrderEventTriggerConfig",
"MasterLabel": "MyOrderEventTriggerConfig",
"PlatformEventConsumerId": "<Apex_Trigger_Id>",
"PartitionKey": "Order_Event__e.Order_Number__c",
"NumPartitions": "3"
}
For custom fields, the partition key format is EventName__e.FieldName__c. For the standard EventUuid field, it’s simply EventUuid.
To track your parallel subscriptions, go to Setup, search for Platform Events, and select your platform event. The related list for Parallel Subscriptions will show the configured subscriptions.
Lightning App Real-Time Preview (Beta)
Starting with the Salesforce Winter ’25 release, an exciting new feature is available in open beta. With Local Dev, you can build your Lightning web components (LWCs) and see a real-time preview of your Lightning app or Experience Cloud site. The preview updates automatically in your browser whenever Local Dev detects changes in the source code, allowing you to work on your LWCs faster without needing to deploy code or refresh the page.
To enable Local Dev for your org, go to Setup, type Local Dev in the Quick Find box, and select it. Then, choose Enable Local Dev (Beta) to activate it for all users.
Reminder: currently, you can only use this feature through the command-line interface (CLI).
Final Thoughts
The Salesforce Winter ’25 release brings many new features to help developers create better applications. Here are ENWAY’s top picks from the release. Be sure to read the release notes and check out all the updates for yourself.