# Free Salesforce Platform Developer II Practice Questions

*Last updated September 2026 · https://www.certifyforce.com/salesforce-platform-developer-2-practice-exam/free-questions*

18 free Platform Developer II practice questions from the CertifyForce bank of 300 reviewed questions, spread across the 5 official exam sections in proportion to their weight. Each has the correct answer, an explanation, and the official documentation it was verified against.

## Advanced Developer Fundamentals (15%)

### Question 1

Northern Trail Outfitters shares each Expedition__c record, whose organization-wide default is Private, with every member of the expedition's crew. The current trigger inserts one Expedition__Share row for each crew member on each of 60,000 expeditions. Crews have up to 300 members, people change crews daily, and every change inserts or deletes thousands of share rows. The developer plans to create one public group for each crew. Which two steps should the new design include? Choose 2 answers.

- A. Insert one Expedition__Share row per expedition, with the crew's group ID in `UserOrGroupId` and an Apex sharing reason in `RowCause`.
- B. Set `AccessLevel` to `All` on each group share so that crew members can also transfer, share, and delete the expeditions they work on.
- C. Have Apex create a criteria-based sharing rule for each new crew so that the platform maintains the group shares automatically.
- D. When a person joins or leaves a crew, insert or delete that user's GroupMember record instead of changing Expedition__Share rows.
- E. Update the `UserOrGroupId` of each existing Expedition__Share row to the new group's ID instead of inserting new share rows.

**Correct answers:** A. Insert one Expedition__Share row per expedition, with the crew's group ID in `UserOrGroupId` and an Apex sharing reason in `RowCause`.; D. When a person joins or leaves a crew, insert or delete that user's GroupMember record instead of changing Expedition__Share rows.

**Explanation:** A share row's `UserOrGroupId` can reference a public group, so sharing each expedition once with the crew's group, using an Apex sharing reason, replaces hundreds of per-user rows. Crew changes then only add or remove GroupMember records, which represent the members of a public group, and the expedition shares stay the same; because GroupMember is a setup object, that DML must run in a separate transaction from DML on records such as Expedition__c. The `All` access level can't be granted through Apex. Apex can't create criteria-based sharing rules. `UserOrGroupId` can't be updated on an existing share row, so old rows must be deleted and new ones inserted.

**Sources:** [Sharing a Record Using Apex](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_bulk_sharing_creating_with_apex.htm), [Understanding Sharing](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_bulk_sharing_understanding.htm), [GroupMember](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_groupmember.htm), [sObjects That Can't Be Used Together in DML Operations](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_dml_non_mix_sobjects.htm)

### Question 2

Universal Containers has multiple currencies enabled. A developer at Universal Containers reviews this query, which runs for a user whose personal currency is EUR:
```soql
SELECT Name, Amount, convertCurrency(Amount) convertedAmount
FROM Opportunity
WHERE Amount > USD50000
```
Which two statements about the query are true? Choose 2 answers.

- A. `Amount` is returned in the org's corporate currency.
- B. The query fails because the result of `convertCurrency()` can't be given an alias.
- C. `convertedAmount` contains each opportunity's amount converted to the user's currency, EUR.
- D. The filter returns opportunities whose amount is greater than the equivalent of 50,000 US dollars, whatever each record's currency is.
- E. The filter compares the number 50,000 to each amount without taking the record's currency into account.

**Correct answers:** C. `convertedAmount` contains each opportunity's amount converted to the user's currency, EUR.; D. The filter returns opportunities whose amount is greater than the equivalent of 50,000 US dollars, whatever each record's currency is.

**Explanation:** `convertCurrency()` in the SELECT clause converts currency fields to the user's currency, and it supports an alias such as `convertedAmount`. In the WHERE clause, `convertCurrency()` isn't allowed, so SOQL uses an ISO-code prefix: `Amount > USD50000` returns records whose amount is greater than the equivalent of USD 50,000. Only a filter written without the ISO code compares the plain number 50,000 to each amount regardless of the record's currency, so the statement that the filter ignores the record's currency is wrong. The unwrapped `Amount` field comes back in the record's own currency, not the corporate currency, and aliasing `convertCurrency()` doesn't cause an error.

**Sources:** [convertCurrency()](https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_querying_currency_fields.htm), [Manage Multiple Currencies](https://help.salesforce.com/s/articleView?id=sales.admin_currency.htm&language=en&type=5)

### Question 3

Ursa Major Solar's Lightning web component lists the service regions stored in the Service_Region__mdt custom metadata type. It calls this Apex method:
```apex
public with sharing class RegionController {
    @AuraEnabled(cacheable=true)
    public static List<Service_Region__mdt> getRegions() {
        return [SELECT DeveloperName, Label, Region_Code__c FROM Service_Region__mdt];
    }
}
```
The org uses the default Schema Settings. The component shows the regions to administrators, but it doesn't work for field technicians. The security team doesn't want to grant technicians access to the custom metadata type. What should the developer do?

- A. Declare the class `without sharing` so that the query runs in system mode and returns the records to every user
- B. Add `WITH SYSTEM_MODE` to the query so that the technicians' access to the custom metadata type isn't checked
- C. Copy the field values into a wrapper class or a list of strings, and return that instead of the records
- D. Replace the query with `Service_Region__mdt.getAll().values()`, which doesn't count against SOQL limits

**Correct answer:** C. Copy the field values into a wrapper class or a list of strings, and return that instead of the records

**Explanation:** The Restrict access to custom metadata types preference is enabled by default, so users need access granted through profiles or permission sets, and functionality that runs in user mode, such as an Aura or Lightning component receiving `Service_Region__mdt` records from an `@AuraEnabled` method, checks that access. Apex runs in system mode and isn't affected by the preference, so returning the values in a wrapper class or as strings works, because the result is no longer the custom metadata type. The `with sharing` keyword doesn't affect access to custom metadata types, and the query already runs in system mode, so neither `without sharing` nor `WITH SYSTEM_MODE` changes what the component receives. `getAll()` still returns Service_Region__mdt records, so the same access check applies.

**Sources:** [Protection and Privacy Options for Custom Metadata Types](https://help.salesforce.com/s/articleView?id=platform.custommetadatatypes_protection.htm&language=en&type=5)

## Process Automation, Logic, and Integration (27%)

### Question 4

AW Computing sells a managed package that calculates shipping costs. Subscribers can supply their own calculation logic by writing a global class that implements the package's global `ShippingCalculator` interface, and they enter the class name in a custom metadata record. How should the package instantiate the subscriber's class at run time?

- A. Call `Schema.getGlobalDescribe().get(className)` and cast the result to `ShippingCalculator`.
- B. Get the class's type with `Type.forName`, then call `newInstance()` and cast the result to `ShippingCalculator`.
- C. Query the `ApexClass` object for the class body and run it with `Database.query`.
- D. Use dynamic DML to insert a record of the subscriber class type and cast it to `ShippingCalculator`.

**Correct answer:** B. Get the class's type with `Type.forName`, then call `newInstance()` and cast the result to `ShippingCalculator`.

**Explanation:** `Type.forName` returns the `System.Type` for a public or global class name, and `newInstance()` creates an instance that can be cast to the interface. Salesforce documents this pattern for packages that read the name of an implementation class from the subscriber's configuration. From an installed managed package, use the two-argument `Type.forName(namespace, name)` with an empty or null namespace when the subscriber org has no namespace, because the one-argument form returns null there. `Schema.getGlobalDescribe()` returns only sObject tokens, not Apex classes. Querying `ApexClass` returns metadata about a class, and `Database.query` runs SOQL, not Apex code. Dynamic DML creates and saves sObject records and can't instantiate Apex classes.

**Sources:** [Apex Reference Guide: Type Class](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_methods_system_type.htm), [Dynamic Apex](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_dynamic.htm)

### Question 5

Refer to the query below:
```soql
SELECT LeadSource, COUNT(Id) cnt
FROM Lead
GROUP BY ROLLUP(LeadSource)
```
A developer at Northern Trail Outfitters loops through the AggregateResult rows to build a chart that must include leads with no lead source. Two rows have a null LeadSource: one for leads with no lead source and one grand total. How should the developer identify the grand-total row?

- A. Add GROUPING(LeadSource) to the SELECT list and treat the row where it returns 1 as the grand-total row.
- B. Change the clause to GROUP BY CUBE(LeadSource), which labels subtotal rows with the text Total.
- C. Add WHERE LeadSource != null so that only the grand-total row has a null LeadSource.
- D. Treat the first row returned as the grand total, because ROLLUP always returns subtotal rows first.

**Correct answer:** A. Add GROUPING(LeadSource) to the SELECT list and treat the row where it returns 1 as the grand-total row.

**Explanation:** GROUP BY ROLLUP adds subtotal rows whose grouping field is null, and GROUPING(fieldName) returns 1 when a row is a subtotal for that field and 0 otherwise, which Salesforce describes as the best way to differentiate aggregated data from subtotals. GROUP BY CUBE also returns subtotal rows with null grouping values; it doesn't add a Total label. Filtering out null LeadSource values in WHERE removes the leads with no lead source from the counts, which breaks the chart requirement and changes the grand total. Result order isn't guaranteed without ORDER BY, and in the documented examples the grand total appears as the last row, not the first.

**Sources:** [Identify Subtotals with GROUP BY](https://developer.salesforce.com/docs/platform/salesforce-soql-sosl/guide/sforce-api-calls-soql-select-groupby-grouping.html), [GROUP BY ROLLUP](https://developer.salesforce.com/docs/platform/salesforce-soql-sosl/guide/sforce-api-calls-soql-select-groupby-rollup.html)

### Question 6

A mobile app at Cloud Kicks must send one HTTP request to Salesforce that creates a Store_Visit__c record, creates a Visit_Note__c record that looks up to the new visit, and updates the existing Account record's `Last_Visit__c` lookup with the new visit's ID. If any step fails, none of the changes can be saved, and the team doesn't want to write custom Apex. Which REST API approach should the developer use?

- A. The Composite Batch resource with `haltOnError` set to true and one subrequest for each of the three records
- B. The Composite resource with `allOrNone` set to true and `@{refVisit.id}` references in later subrequests
- C. The sObject Tree resource, with the visit note nested under the visit and the account nested under the note
- D. The sObject Collections resource, sending all three records in a single request with `allOrNone` set to true

**Correct answer:** B. The Composite resource with `allOrNone` set to true and `@{refVisit.id}` references in later subrequests

**Explanation:** The Composite resource runs a series of subrequests in one call, and a later subrequest can use the output of an earlier one through its reference ID, such as `@{refVisit.id}`. With `allOrNone` set to true, an error in any subrequest rolls back the entire composite request. Composite Batch subrequests are independent and can't pass information to each other, and `haltOnError` only stops the remaining subrequests; commits made by earlier subrequests aren't rolled back. sObject Tree creates new parent-child record trees, so it can't update the existing account. An sObject Collections request performs one kind of operation, such as create with POST or update with PATCH, so it can't create the visit and update the account together.

**Sources:** [Send Multiple Requests Using Composite](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_composite_composite_post.htm), [Composite Request Body](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/requests_composite.htm), [Composite Batch](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_composite_batch.htm), [sObject Tree](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_composite_sobject_tree.htm), [Create Records Using sObject Collections](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_composite_sobjects_collections_create.htm)

### Question 7

At Universal Containers, an after insert trigger on Order publishes an `Order_Placed__e` platform event, which uses the default publish behavior. Sometimes a subscriber can't find the order when it queries for it. Events are also received for orders whose transaction later failed and rolled back. What should the developer do?

- A. Change the event's publish behavior to Publish After Commit.
- B. Call `Database.setSavepoint()` before `EventBus.publish()` and roll back the event if the order fails.
- C. Increase the batch size of the subscriber's Apex trigger to 2,000 events.
- D. Move the `EventBus.publish()` call from the after insert trigger to a before insert trigger.

**Correct answer:** A. Change the event's publish behavior to Publish After Commit.

**Explanation:** With Publish Immediately, the default, the event message is published as soon as the publish call runs, whether or not the transaction succeeds. A subscriber can therefore receive it before the order is committed, or for an order that's rolled back. Publish After Commit publishes the message only after the transaction commits successfully, and doesn't publish it if the transaction fails. A savepoint can't undo a Publish Immediately message, because the message is published when the publish call runs, regardless of whether the transaction succeeds. The subscriber's batch size doesn't affect when messages are published. A before insert trigger runs in the same uncommitted transaction, so moving the publish call there doesn't fix the timing.

**Sources:** [Publish Event Messages with Apex](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_publish_apex.htm), [Platform Event Fields](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_define_ui.htm), [Enterprise Messaging Platform Events](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_intro_emp.htm)

### Question 8

Get Cloudy Consulting has one large record-triggered flow on Shipment__c with loops, collection filters, and cross-object updates. During nightly Bulk API loads of 10,000 records, the flow hits CPU time limits. The architect proposes moving this logic to an Apex trigger framework. Which two considerations support the proposal? Choose 2 answers.

- A. Apex offers Map and Set collections and finer control over queries, which make bulk processing more efficient than Flow loops.
- B. Apex can cache expensive query results in static variables and reuse them across invocations in the same transaction.
- C. Splitting the flow into several smaller, well-conditioned flows would remove the CPU time problem without any code.
- D. Apex triggers are exempt from governor limits because Apex code in triggers runs in system mode.
- E. Record-triggered flows can't process more than 200 records from a single Bulk API load.

**Correct answers:** A. Apex offers Map and Set collections and finer control over queries, which make bulk processing more efficient than Flow loops.; B. Apex can cache expensive query results in static variables and reuse them across invocations in the same transaction.

**Explanation:** Salesforce's automation guidance rates Apex as the recommended tool for high-performance bulk processing because it provides Map and Set data structures and more control over query optimization, while Flow lacks native maps and sets. Apex can also deduplicate expensive work by caching data in static variables for the duration of the transaction, something Flow can't do across invocations. Splitting a mega-flow into multiple flows doesn't have a major performance impact; the gains come from before-save updates and precise entry conditions. Apex in triggers is always subject to governor limits; system mode concerns data access, not limits. Flows do run for Bulk API loads, which are processed in chunks rather than rejected.

**Sources:** [Record-Triggered Automation Decision Guide (Salesforce Architects)](https://architect.salesforce.com/docs/architect/decision-guides/guide/record-triggered), [Execution Governors and Limits](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_gov_limits.htm)

## User Interface (20%)

### Question 9

Northern Trail Outfitters has a Lightning web component that shows quote line items and lets a sales rep apply a discount. `getLineItems` is a cacheable Apex method, and `applyDiscount` updates the line items. Refer to the component's JavaScript:
```javascript
import { refreshApex } from '@salesforce/apex';
// getLineItems and applyDiscount are imported from QuoteController
export default class QuoteLines extends LightningElement {
    @api recordId;
    lineItems;
    @wire(getLineItems, { quoteId: '$recordId' })
    wiredLines({ data }) {
        if (data) { this.lineItems = data; }
    }
    async handleDiscount() {
        await applyDiscount({ quoteId: this.recordId });
        await refreshApex(this.lineItems);
    }
}
```
The discount is saved, but the component keeps showing the old prices until the page is reloaded. What should the developer do?

- A. Add `cacheable=true` to `applyDiscount` so that both methods share the same client-side cache.
- B. Replace the `refreshApex()` call with `notifyRecordUpdateAvailable()` for the quote record.
- C. Store the value the wire service passes to `wiredLines` in a property, then pass that property to `refreshApex()`.
- D. Decorate `lineItems` with `@track` so the component rerenders when the discount is applied.

**Correct answer:** C. Store the value the wire service passes to `wiredLines` in a property, then pass that property to `refreshApex()`.

**Explanation:** `refreshApex()` must receive an object that an Apex `@wire` previously emitted; for a wired function, that's the argument the function receives, not the `data` extracted from it. Saving that value (for example, `this.wiredResult = result`) and calling `refreshApex(this.wiredResult)` makes the wire requery the server and provision fresh line items. `applyDiscount` changes data, so it can't be marked cacheable, and caching it wouldn't refresh another method's results anyway. `notifyRecordUpdateAvailable()` refreshes records in the Lightning Data Service cache, but Lightning Data Service doesn't manage data provisioned by Apex, so the wired line items would stay stale. `@track` only affects how changes to a field are observed; the real problem is that no new data is requested.

**Sources:** [Client-Side Caching of Apex Method Results](https://developer.salesforce.com/docs/platform/lwc/guide/apex-result-caching.html), [Call Apex Methods](https://developer.salesforce.com/docs/platform/lwc/guide/apex.html)

### Question 10

In a Lightning console app at Cumulus Financial, a Lightning web component on a case workspace subtab subscribes to a message channel:
```javascript
@wire(MessageContext)
messageContext;

connectedCallback() {
    if (!this.subscription) {
        this.subscription = subscribe(
            this.messageContext,
            CASE_ALERT_CHANNEL,
            (message) => this.handleAlert(message)
        );
    }
}
```
The component receives messages only while its subtab is the one the agent is viewing. It must receive alerts even when the agent is working in another tab. What should the developer do?

- A. Pass `{ scope: APPLICATION_SCOPE }`, imported from `lightning/messageService`, as the fourth argument to `subscribe()`.
- B. Pass `{ scope: APPLICATION_SCOPE }` as the fourth argument to `publish()` in the publishing component.
- C. Replace `@wire(MessageContext)` with `createMessageContext()` so the subscription isn't tied to the active tab.
- D. Move the `subscribe()` call into the constructor so the subscription exists before the subtab renders.

**Correct answer:** A. Pass `{ scope: APPLICATION_SCOPE }`, imported from `lightning/messageService`, as the fourth argument to `subscribe()`.

**Explanation:** By default, a Lightning message service subscriber receives messages only in the application's active area, such as the selected navigation or workspace tabs and utility items. To receive messages from anywhere in the application, import `APPLICATION_SCOPE` from `lightning/messageService` and pass `{ scope: APPLICATION_SCOPE }` as the optional fourth `subscriberOptions` argument to `subscribe()`. Scope is a subscriber option, so passing it to `publish()` doesn't change which subscribers receive the message. For Lightning web components, scoping is available only with `@wire(MessageContext)`, and `createMessageContext()` is for API modules that don't extend `LightningElement`. A component must be attached to the DOM before it can use the `MessageContext` property, so subscribing in the constructor is too early.

**Sources:** [Define the Scope of the Message Service](https://developer.salesforce.com/docs/platform/lwc/guide/use-message-channel-scope.html), [Subscribe and Unsubscribe from a Message Channel](https://developer.salesforce.com/docs/platform/lwc/guide/use-message-channel-subscribe.html)

### Question 11

A developer at Get Cloudy Consulting places eight project tiles inside `<lightning-layout multiple-rows>`. Each tile is wrapped in this layout item:
```html
<lightning-layout-item size="12" small-device-size="6" large-device-size="3">
    <c-project-tile></c-project-tile>
</lightning-layout-item>
```
How wide is each tile when a user views the component on a tablet?

- A. Full width, because `size="12"` applies to every device type.
- B. One quarter of the width, because `large-device-size` applies to tablets and larger devices.
- C. The width of its content, because the device-size attributes are ignored when `medium-device-size` isn't set.
- D. Half the width, because `small-device-size` applies to devices of that size and larger, and `medium-device-size` isn't set.

**Correct answer:** D. Half the width, because `small-device-size` applies to devices of that size and larger, and `medium-device-size` isn't set.

**Explanation:** For `lightning-layout-item`, `small-device-size` typically targets smart phones, `medium-device-size` targets tablets, and `large-device-size` targets desktops or larger devices. The device-size attributes are additive and apply to devices of the specified size and larger, so with no `medium-device-size`, a tablet uses the `small-device-size` value of 6 out of 12 columns, which is half the width. When both `size` and `small-device-size` are set, `size` applies only to small mobile phones. `large-device-size` applies to desktops and larger, not tablets. A missing `medium-device-size` doesn't make the item fall back to its content width; the smaller device setting carries up.

**Sources:** [Layout Item (lightning-layout-item)](https://developer.salesforce.com/docs/platform/lightning-component-reference/guide/lightning-layout-item.html)

### Question 12

Refer to this Aura component at Northern Trail Outfitters:
```xml
<aura:component implements="flexipage:availableForRecordHome">
    <ltng:require scripts="{!$Resource.mapLib + '/leaflet.js'}"
                  styles="{!$Resource.mapLib + '/leaflet.css'}"/>
    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
    <div aura:id="map" class="trail-map"></div>
</aura:component>
```
The `doInit` action creates the map with the library's global `L` object. Users intermittently see an error that `L` is undefined. What should the developer do?

- A. Replace `$Resource.mapLib + '/leaflet.js'` with `URLFOR($Resource.mapLib, 'leaflet.js')` in the `scripts` attribute.
- B. Add a `<script>` tag that references the static resource directly in the component markup.
- C. Create the map in an action set by `afterScriptsLoaded="{!c.initMap}"` on `ltng:require` instead of in `doInit`.
- D. Load the library in `doInit` by calling `$A.get('$Resource.mapLib')` before the code that creates the map.

**Correct answer:** C. Create the map in an action set by `afterScriptsLoaded="{!c.initMap}"` on `ltng:require` instead of in `doInit`.

**Explanation:** Scripts loaded with `ltng:require` load asynchronously, so they probably aren't available yet when the `init` handler runs. The `afterScriptsLoaded` action runs after the scripts are loaded and the component is rendered, so the map should be created there. Aura has nothing like `URLFOR()`; string concatenation with `$Resource`, as the component already uses, is the correct syntax. You can't use a `<script>` tag in an Aura component, a restriction that mitigates the risk of cross-site scripting attacks. `$A.get('$Resource.mapLib')` only returns the resource's URL; it doesn't load or run the script.

**Sources:** [Using External JavaScript Libraries](https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/js_libs_platform.htm), [$Resource](https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/expr_resource_value_provider.htm)

## Testing, Debugging, and Deployment (20%)

### Question 13

A developer at Universal Containers wrote this Jest test for a `c-greeting` component. Its template renders `<p>Hello, {name}</p>`, and its `name` property is decorated with `@api` and defaults to `'World'`.
```javascript
it('greets the user by name', () => {
    const element = createElement('c-greeting', { is: Greeting });
    document.body.appendChild(element);
    element.name = 'Ada';
    const p = element.shadowRoot.querySelector('p');
    expect(p.textContent).toBe('Hello, Ada');
});
```
The test fails because the paragraph still reads "Hello, World". Which change fixes the test?

- A. Query the paragraph with `document.querySelector('p')` instead of `element.shadowRoot.querySelector('p')`.
- B. Make the test function `async` and add `await Promise.resolve();` after setting `element.name`, before querying the paragraph.
- C. Call `element.connectedCallback()` after setting `element.name` to force the component to rerender.
- D. Decorate `name` with `@track` instead of `@api` so the template updates synchronously.

**Correct answer:** B. Make the test function `async` and add `await Promise.resolve();` after setting `element.name`, before querying the paragraph.

**Explanation:** When a component's state changes after it's inserted into the DOM, the DOM updates asynchronously, so the test must wait for a resolved promise before checking the result. Setting the property before `appendChild()` would also work, because the component then renders synchronously. `document.querySelector()` can't see into the component's shadow tree; `element.shadowRoot` is the test-only API for inspecting it, so that change returns null. Tests don't call lifecycle hooks to force a rerender. Replacing `@api` with `@track` would remove `name` from the component's public API, and rerendering would still be asynchronous.

**Sources:** [Write Jest Tests for Lightning Web Components](https://developer.salesforce.com/docs/platform/lwc/guide/unit-testing-using-jest-create-tests.html), [Jest Test Patterns and Mock Dependencies](https://developer.salesforce.com/docs/platform/lwc/guide/unit-testing-using-jest-patterns.html)

### Question 14

At Northern Trail Outfitters, Shipment_Line__c is the detail object in a master-detail relationship with Shipment__c. A before delete trigger on Shipment_Line__c creates an Archive__c record for each deleted line. Archive records are created when users delete individual lines, but not when a user deletes a whole shipment, even though its lines are deleted too. What is the cause?

- A. Cascading deletes don't invoke triggers on the child records, because only records that initiate a delete cause trigger evaluation; the archiving must also be handled by a delete trigger on Shipment__c.
- B. Before delete triggers on detail objects run only for records deleted through the API, not for deletions made in the user interface.
- C. The Shipment_Line__c trigger runs, but Archive__c records inserted during a cascade delete are rolled back when the master record is deleted.
- D. The trigger must be changed from before delete to after delete, because before delete triggers never run when a parent record's deletion cascades to its children, but after delete triggers do run for the cascaded deletion in that case.

**Correct answer:** A. Cascading deletes don't invoke triggers on the child records, because only records that initiate a delete cause trigger evaluation; the archiving must also be handled by a delete trigger on Shipment__c.

**Explanation:** Some system operations don't invoke triggers, and cascading deletes are one of them: only the records that initiate a delete cause trigger evaluation. When a user deletes a Shipment__c record, its Shipment_Line__c records are removed by the cascade, so no Shipment_Line__c trigger runs. A before delete trigger on Shipment__c can query the lines before they're removed and archive them. The trigger doesn't depend on whether a direct delete came from the API or the UI, no Archive__c records are rolled back because the trigger never runs, and switching to after delete doesn't help for the same reason.

**Sources:** [Operations That Don't Invoke Triggers](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_triggers_ignoring_operations.htm)

### Question 15

Ursa Major Solar's `IncentiveCalculator` class has a private static method, `tierFor(Decimal kilowatts)`, that contains complex branching logic. A developer wants a unit test to call this method directly, but the method must stay inaccessible to other production classes. What should the developer do?

- A. Change the method's access modifier to `public` and add a comment that it's for testing only.
- B. Annotate the test class with `@IsTest(SeeAllData=true)` so it can call private methods.
- C. Annotate the method with `@TestVisible`.
- D. Declare `IncentiveCalculator` as `global` so that test classes can access its private members.

**Correct answer:** C. Annotate the method with `@TestVisible`.

**Explanation:** `@TestVisible` lets test methods access private or protected members of another class, including methods, member variables, and inner classes, and it doesn't change their visibility for non-test code. Making the method `public` exposes it to every class that can see `IncentiveCalculator`, which breaks the requirement. `SeeAllData=true` controls access to org data, not access to class members. Declaring the class `global` widens access to the class itself but doesn't expose its private members.

**Sources:** [TestVisible Annotation](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_annotation_testvisible.htm), [Testing Best Practices](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_testing_best_practices.htm)

## Performance (18%)

### Question 16

The Delivery__c object at Northern Trail Outfitters holds 4 million records. A developer must pick one filter for a query that runs in a trigger, and the query must be selective. Based on the query optimizer's index thresholds, which filter is selective?

- A. `WHERE Region__c = 'West'`, where `Region__c` has a custom index and the filter matches 350,000 records
- B. `WHERE CreatedDate = LAST_N_DAYS:90`, which matches 700,000 records
- C. `WHERE Tracking_Code__c = :code`, where `Tracking_Code__c` is a custom text field without an index and the filter matches 5 records
- D. `WHERE Status__c != 'Delivered'`, where `Status__c` has a custom index and the filter matches 100,000 records

**Correct answer:** B. `WHERE CreatedDate = LAST_N_DAYS:90`, which matches 700,000 records

**Explanation:** A filter is selective only when it's on an indexed field, uses an operator the index supports, and matches fewer records than the index threshold. For a standard index such as CreatedDate, the threshold is 30% of the first million records plus 15% of the remaining records, up to 1 million; for 4 million records that's 750,000, so a filter that matches 700,000 records qualifies. A custom index is used only when the filter matches less than 10% of the records, up to a maximum of 333,333, so 350,000 matches are too many. A filter on an unindexed field isn't selective however few records it matches, because the optimizer has no index to use. A custom index typically isn't used with a negative operator such as `!=`.

**Sources:** [Indexes (Large Data Volumes Best Practices)](https://developer.salesforce.com/docs/atlas.en-us.salesforce_large_data_volumes_bp.meta/salesforce_large_data_volumes_bp/ldv_deployments_infrastructure_indexes.htm), [Working with Very Large SOQL Queries](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/langCon_apex_SOQL_VLSQ.htm)

### Question 17

A before insert trigger handler at Cloud Kicks sets a field on each new Order__c record based on the running user's role:
```apex
for (Order__c o : orders) {
    User u = [SELECT UserRoleId FROM User WHERE Id = :UserInfo.getUserId()];
    o.Created_By_Role__c = u.UserRoleId;
}
```
Imports of more than 100 orders fail with `System.LimitException: Too many SOQL queries: 101`. Which change removes the query from the transaction entirely, rather than just moving it outside the loop?

- A. Add `LIMIT 1` to the query so that each iteration retrieves only one row.
- B. Move the query into a `@future` method so that it runs with higher governor limits.
- C. Query all users with `SELECT Id, UserRoleId FROM User` before the loop, and search the list for the running user.
- D. Call `UserInfo.getUserRoleId()` once before the loop, and assign the stored value inside the loop.

**Correct answer:** D. Call `UserInfo.getUserRoleId()` once before the loop, and assign the stored value inside the loop.

**Explanation:** The query returns the same value for every order, yet it runs once per record. `UserInfo.getUserRoleId()` returns the context user's role ID without a SOQL query, and calling it once before the loop removes the repeated work entirely. `LIMIT 1` still runs one query per order, so the limit is still exceeded. A future method runs later in a separate transaction, so it can't supply the value before the orders are saved. Querying every user retrieves many unnecessary rows and adds a search through the list, when the value is already available without any query.

**Sources:** [UserInfo Class](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_methods_system_userinfo.htm), [Trigger and Bulk Request Best Practices](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_triggers_bestpract.htm)

### Question 18

A developer at Get Cloudy Consulting built a Lightning web component for the Project__c record page that shows the project's status, budget, and end date. The component uses this markup:
```html
<lightning-record-form
    record-id={recordId}
    object-api-name="Project__c"
    layout-type="Full"
    mode="readonly">
</lightning-record-form>
```
The Project__c page layout contains more than 150 fields, and the component loads slowly. What should the developer do?

- A. Replace the component with an Apex method that queries every Project__c field.
- B. Replace `layout-type` with a `fields` attribute that lists only the three fields the component displays.
- C. Change `mode` to "view" so that the form loads in view mode.
- D. Wrap the form in a `<template lwc:if>` whose condition is set to true in `renderedCallback()`.

**Correct answer:** B. Replace `layout-type` with a `fields` attribute that lists only the three fields the component displays.

**Explanation:** Salesforce recommends specifying fields instead of a layout whenever possible to improve performance, because a layout makes the component load every field that the administrator assigns to it. Listing only the three fields in the `fields` attribute reduces the data that the form requests and renders. Querying every field in Apex moves the same problem to the server and gives up Lightning Data Service caching. Changing the mode controls whether users can edit fields inline, not how many fields load. Rendering the form after `renderedCallback()` only delays it and triggers an extra render.

**Sources:** [Data Guidelines (Lightning Web Components Developer Guide)](https://developer.salesforce.com/docs/platform/lwc/guide/data-guidelines.html), [Lightning Web Components Performance Best Practices](https://developer.salesforce.com/blogs/2020/06/lightning-web-components-performance-best-practices)

## Related pages

- [Salesforce Platform Developer II practice exam](https://www.certifyforce.com/salesforce-platform-developer-2-practice-exam)
- [Practice exam packs](https://www.certifyforce.com/practice-exams)
- [Official Platform Developer II exam guide](https://help.salesforce.com/s/articleView?id=005298967&language=en_US&type=1)
