Skip to content

Commit b12a95b

Browse files
committed
Mocking QueryException
1 parent f4b60a6 commit b12a95b

File tree

5 files changed

+315
-1
lines changed

5 files changed

+315
-1
lines changed

force-app/main/default/classes/main/standard-soql/SOQL.cls

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,9 @@ public virtual inherited sharing class SOQL implements Queryable {
417417
Mockable thenReturn(Map<String, Object> aggregatedResult);
418418
// Count
419419
Mockable thenReturn(Integer count);
420+
// Exception
421+
void throwException();
422+
void throwException(String message);
420423
}
421424

422425
// Backward support - it's going to be removed in the future
@@ -2402,6 +2405,7 @@ public virtual inherited sharing class SOQL implements Queryable {
24022405
public SObjectMock sObjectMock = new SObjectMock();
24032406
public CountMock countMock = new CountMock();
24042407
public AggregateResultProxies aggregateResultMock = new AggregateResultProxies();
2408+
public ExceptionMock exceptionMock = null;
24052409

24062410
public Mockable thenReturn(List<Map<String, Object>> aggregatedResults) {
24072411
this.aggregateResultMock.add(aggregatedResults);
@@ -2428,6 +2432,19 @@ public virtual inherited sharing class SOQL implements Queryable {
24282432
return this;
24292433
}
24302434

2435+
public void throwException() {
2436+
throwException('List has no rows for assignment to SObject');
2437+
}
2438+
2439+
public void throwException(String message) {
2440+
this.exceptionMock = new ExceptionMock();
2441+
this.exceptionMock.set(new QueryException(message));
2442+
}
2443+
2444+
public Boolean hasExceptionMock() {
2445+
return this.exceptionMock != null;
2446+
}
2447+
24312448
public SoqlMock useLegacyMockingBehavior() {
24322449
this.sObjectMock.useLegacyMockingBehavior = true;
24332450
return this;
@@ -2568,6 +2585,18 @@ public virtual inherited sharing class SOQL implements Queryable {
25682585
}
25692586
}
25702587

2588+
private class ExceptionMock {
2589+
private QueryException queryException;
2590+
2591+
public void set(QueryException queryException) {
2592+
this.queryException = queryException;
2593+
}
2594+
2595+
public QueryException get() {
2596+
return this.queryException;
2597+
}
2598+
}
2599+
25712600
private inherited sharing class Executor {
25722601
private DatabaseQuery sharingExecutor;
25732602
private AccessLevel accessMode;
@@ -2619,6 +2648,7 @@ public virtual inherited sharing class SOQL implements Queryable {
26192648
this.incrementQueryIssued();
26202649

26212650
if (!this.mocks.isEmpty()) {
2651+
this.throwExceptionMockIfExists();
26222652
return this.getMockedListProxy();
26232653
}
26242654

@@ -2632,6 +2662,12 @@ public virtual inherited sharing class SOQL implements Queryable {
26322662
).getRecords();
26332663
}
26342664

2665+
private void throwExceptionMockIfExists() {
2666+
if (this.mocks[0].hasExceptionMock()) {
2667+
throw this.mocks[0].exceptionMock.get();
2668+
}
2669+
}
2670+
26352671
private List<SObject> getMockedListProxy() {
26362672
if (this.mocks.size() == 1) {
26372673
return this.mocks[0].sObjectMock.get(this.builder.fields, this.builder.subQueries);
@@ -2643,6 +2679,7 @@ public virtual inherited sharing class SOQL implements Queryable {
26432679
this.incrementQueryIssued();
26442680

26452681
if (!this.mocks.isEmpty()) {
2682+
this.throwExceptionMockIfExists();
26462683
return this.getMockedAggregateProxy();
26472684
}
26482685

@@ -2660,6 +2697,7 @@ public virtual inherited sharing class SOQL implements Queryable {
26602697
this.incrementQueryIssued();
26612698

26622699
if (!this.mocks.isEmpty()) {
2700+
this.throwExceptionMockIfExists();
26632701
return this.getMockedCount();
26642702
}
26652703

force-app/main/default/classes/main/standard-soql/SOQL_Test.cls

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3522,6 +3522,43 @@ private class SOQL_Test {
35223522
Assert.isNotNull(result.Id, 'The result account Id should be always set even if the mocked account Id is not set.');
35233523
}
35243524

3525+
@IsTest
3526+
static void mockingQueryException() {
3527+
// Test
3528+
SOQL.mock('mockingQuery').throwException();
3529+
3530+
Exception queryException = null;
3531+
3532+
try {
3533+
Account result = (Account) SOQL.of(Account.SObjectType).with(Account.Name).mockId('mockingQuery').toObject();
3534+
} catch (QueryException e) {
3535+
queryException = e;
3536+
}
3537+
3538+
// Verify
3539+
Assert.isNotNull(queryException, 'The query exception should be thrown, because it\'s mocked.');
3540+
Assert.areEqual('List has no rows for assignment to SObject', queryException.getMessage(), 'The query exception should be thrown.');
3541+
}
3542+
3543+
@IsTest
3544+
static void mockingQueryExceptionWithMessage() {
3545+
// Test
3546+
String message = 'No such column \'Name\' on entity \'Account\'.';
3547+
SOQL.mock('mockingQuery').throwException(message);
3548+
3549+
Exception queryException = null;
3550+
3551+
try {
3552+
Account result = (Account) SOQL.of(Account.SObjectType).with(Account.Name).mockId('mockingQuery').toObject();
3553+
} catch (QueryException e) {
3554+
queryException = e;
3555+
}
3556+
3557+
// Verify
3558+
Assert.isNotNull(queryException, 'The query exception should be thrown, because it\'s mocked.');
3559+
Assert.areEqual(message, queryException.getMessage(), 'The query exception should be thrown.');
3560+
}
3561+
35253562
@IsTest
35263563
static void mockingCount() {
35273564
// Test

website/docs/soql/advanced/mocking.md

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ public class ExampleControllerTest {
381381

382382
We are using field aliasing: https://salesforce.stackexchange.com/questions/393308/get-a-list-of-one-column-from-a-soql-result
383383

384-
Its approximately 2x more efficient than a standard for loop. Because of this, mocking works differently for the following methods:
384+
It's approximately 2x more efficient than a standard for loop. Because of this, mocking works differently for the following methods:
385385

386386
- `toIdsOf(SObjectField field)`
387387
- `toIdsOf(String relationshipName, SObjectField field)`
@@ -425,3 +425,92 @@ public class ExampleControllerTest {
425425
}
426426
}
427427
```
428+
429+
## Mocking Exceptions
430+
431+
SOQL Lib supports mocking query exceptions to test error handling scenarios in your code. This is crucial for validating that your application gracefully handles database errors, security violations, and other query-related exceptions.
432+
433+
### Default Exception
434+
435+
The `.throwException()` method simulates a standard query exception with the default message: **"List has no rows for assignment to SObject"**.
436+
437+
This is useful for testing code paths that handle empty query results or unavailable data.
438+
439+
```apex title="ExampleController.cls"
440+
public with sharing class ExampleController {
441+
public static Account getAccountById(Id accountId) {
442+
try {
443+
return (Account) SOQL.of(Account.SObjectType)
444+
.with(Account.Name, Account.BillingCity)
445+
.byId(accountId)
446+
.mockId('ExampleController.getAccountById')
447+
.toObject();
448+
} catch (Exception e) {
449+
// Logger here
450+
throw e;
451+
}
452+
}
453+
}
454+
```
455+
456+
```apex title="ExampleControllerTest.cls"
457+
@IsTest
458+
static void getAccountByIdException() {
459+
SOQL.mock('ExampleController.getAccountById').throwException();
460+
461+
Test.startTest();
462+
Exception error;
463+
try {
464+
Account result = ExampleController.getAccountById('001000000000000AAA');
465+
} catch (Exception e) {
466+
error = e;
467+
}
468+
Test.stopTest();
469+
470+
Assert.isNotNull(error, 'The query exception should be thrown.');
471+
}
472+
```
473+
474+
### Custom Exception Message
475+
476+
Use `.throwException(message)` to simulate a query exception with a custom error message, such as field-level security errors or invalid field references.
477+
478+
```apex title="Controller with Error Handling"
479+
public with sharing class ExampleController {
480+
public static Account getAccountById(Id accountId) {
481+
try {
482+
return (Account) SOQL.of(Account.SObjectType)
483+
.with(Account.Name, Account.BillingCity)
484+
.byId(accountId)
485+
.mockId('ExampleController.getAccountById')
486+
.toObject();
487+
} catch (Exception e) {
488+
// Logger here
489+
throw e;
490+
}
491+
}
492+
}
493+
```
494+
495+
```apex title="Unit Test with Default Exception Mock"
496+
@IsTest
497+
public class ExampleControllerTest {
498+
@IsTest
499+
static void getAccountByIdException() {
500+
String errorMessage = 'No such column \'InvalidField__c\' on entity \'Account\'.';
501+
SOQL.mock('ExampleController.getAccountById').throwException(errorMessage);
502+
503+
Test.startTest();
504+
Exception error;
505+
try {
506+
Account result = ExampleController.getAccountById('001000000000000AAA');
507+
} catch (Exception e) {
508+
error = e;
509+
}
510+
Test.stopTest();
511+
512+
Assert.isNotNull(error, 'The query exception should be thrown.');
513+
}
514+
}
515+
```
516+

website/docs/soql/api/soql.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@ The following are methods for using `SOQL`:
173173
- [`SOQL.mock(String mockId).thenReturn(SObject record)`](#record-mock)
174174
- [`SOQL.mock(String mockId).thenReturn(List<SObject> records)`](#record-mock)
175175
- [`SOQL.mock(String mockId).thenReturn(Integer amount)`](#count-mock)
176+
- [`SOQL.mock(String mockId).throwException()`](#exception-mock)
177+
- [`SOQL.mock(String mockId).throwException(String message)`](#exception-mock-with-message)
176178

177179
[**DEBUGGING**](#debugging)
178180

@@ -2203,6 +2205,68 @@ List<SOQL.AggregateResultProxy> result = SOQL.of(Lead.SObjectType)
22032205
.toAggregatedProxy();
22042206
```
22052207

2208+
### exception mock
2209+
2210+
Mock a query exception with default message.
2211+
2212+
**Signature**
2213+
2214+
```apex
2215+
SOQL.Mockable mock(String mockId).throwException()
2216+
```
2217+
2218+
**Example**
2219+
2220+
```apex
2221+
// In Unit Test
2222+
SOQL.mock('MyQuery').throwException();
2223+
2224+
Test.startTest();
2225+
Exception error;
2226+
try {
2227+
Account result = SOQL.of(Account.SObjectType)
2228+
.mockId('MyQuery')
2229+
.toObject();
2230+
} catch (Exception e) {
2231+
error = e;
2232+
}
2233+
Test.stopTest();
2234+
2235+
Assert.isNotNull(error, 'The query exception should be thrown.');
2236+
```
2237+
2238+
### exception mock with message
2239+
2240+
Mock a query exception with custom error message.
2241+
2242+
**Signature**
2243+
2244+
```apex
2245+
SOQL.Mockable mock(String mockId).throwException(String message)
2246+
```
2247+
2248+
**Example**
2249+
2250+
```apex
2251+
// In Unit Test
2252+
String errorMessage = 'No such column \'InvalidField__c\' on entity \'Account\'.';
2253+
SOQL.mock('MyQuery').throwException(errorMessage);
2254+
2255+
Test.startTest();
2256+
Exception error;
2257+
try {
2258+
Account result = SOQL.of(Account.SObjectType)
2259+
.mockId('MyQuery')
2260+
.toObject();
2261+
} catch (Exception e) {
2262+
error = e;
2263+
}
2264+
Test.stopTest();
2265+
2266+
Assert.isNotNull(error, 'The query exception should be thrown.');
2267+
Assert.isTrue(error.getMessage().contains('InvalidField__c'));
2268+
```
2269+
22062270
## DEBUGGING
22072271
### preview
22082272

0 commit comments

Comments
 (0)