Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion force-app/main/default/classes/cached-soql/SOQLCache.cls
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) 2025 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev)
* Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/soql-lib/blob/main/LICENSE)
*
* v6.1.0
* v6.2.0
*
* PMD False Positives:
* - ExcessivePublicCount: It is a library class and exposes all necessary methods to construct a query
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Copyright (c) 2025 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev)
* Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/soql-lib/blob/main/LICENSE)
*
* v6.1.0
* v6.2.0
*
* PMD False Positives:
* - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) 2025 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev)
* Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/soql-lib/blob/main/LICENSE)
*
* v6.1.0
* v6.2.0
*
* PMD False Positives:
* - CognitiveComplexity: It is a library and we tried to put everything into ONE class
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Copyright (c) 2025 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev)
* Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/soql-lib/blob/main/LICENSE)
*
* v6.1.0
* v6.2.0
*
* PMD False Positives:
* - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class
Expand Down
46 changes: 42 additions & 4 deletions force-app/main/default/classes/standard-soql/SOQL.cls
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) 2025 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev)
* Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/soql-lib/blob/main/LICENSE)
*
* v6.1.0
* v6.2.0
*
* PMD False Positives:
* - ExcessivePublicCount: It is a library class and exposes all necessary methods to construct a query
Expand Down Expand Up @@ -417,6 +417,9 @@ public virtual inherited sharing class SOQL implements Queryable {
Mockable thenReturn(Map<String, Object> aggregatedResult);
// Count
Mockable thenReturn(Integer count);
// Exception
void throwException();
void throwException(String message);
}

// Backward support - it's going to be removed in the future
Expand Down Expand Up @@ -1045,7 +1048,7 @@ public virtual inherited sharing class SOQL implements Queryable {
this.whereAre(Filter.with(relationshipName, targetKeyField).isNotNull());
return this.converter.transform(this.executor.toList()).toAggregatedIdMapBy(relationshipName, targetKeyField);
}

public Map<String, SObject> toMap(SObjectField keyField) {
this.with(keyField);
this.whereAre(Filter.with(keyField).isNotNull());
Expand Down Expand Up @@ -2402,6 +2405,7 @@ public virtual inherited sharing class SOQL implements Queryable {
public SObjectMock sObjectMock = new SObjectMock();
public CountMock countMock = new CountMock();
public AggregateResultProxies aggregateResultMock = new AggregateResultProxies();
public ExceptionMock exceptionMock = null;

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

public void throwException() {
throwException('List has no rows for assignment to SObject');
}

public void throwException(String message) {
this.exceptionMock = new ExceptionMock();
this.exceptionMock.set(new QueryException(message));
}

public Boolean hasExceptionMock() {
return this.exceptionMock != null;
}

public SoqlMock useLegacyMockingBehavior() {
this.sObjectMock.useLegacyMockingBehavior = true;
return this;
Expand Down Expand Up @@ -2568,6 +2585,18 @@ public virtual inherited sharing class SOQL implements Queryable {
}
}

private class ExceptionMock {
private QueryException queryException;

public void set(QueryException queryException) {
this.queryException = queryException;
}

public QueryException get() {
return this.queryException;
}
}

private inherited sharing class Executor {
private DatabaseQuery sharingExecutor;
private AccessLevel accessMode;
Expand Down Expand Up @@ -2619,6 +2648,7 @@ public virtual inherited sharing class SOQL implements Queryable {
this.incrementQueryIssued();

if (!this.mocks.isEmpty()) {
this.throwExceptionMockIfExists();
return this.getMockedListProxy();
}

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

private void throwExceptionMockIfExists() {
if (this.mocks[0].hasExceptionMock()) {
throw this.mocks[0].exceptionMock.get();
}
}

private List<SObject> getMockedListProxy() {
if (this.mocks.size() == 1) {
return this.mocks[0].sObjectMock.get(this.builder.fields, this.builder.subQueries);
Expand All @@ -2643,6 +2679,7 @@ public virtual inherited sharing class SOQL implements Queryable {
this.incrementQueryIssued();

if (!this.mocks.isEmpty()) {
this.throwExceptionMockIfExists();
return this.getMockedAggregateProxy();
}

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

if (!this.mocks.isEmpty()) {
this.throwExceptionMockIfExists();
return this.getMockedCount();
}

Expand Down Expand Up @@ -2930,7 +2968,7 @@ public virtual inherited sharing class SOQL implements Queryable {
public Map<String, List<String>> toAggregatedMap(SObjectField keyField, String relationshipName, SObjectField targetKeyField) {
Map<String, List<String>> customValuesPerCustomKey = new Map<String, List<String>>();
List<String> relationshipPathFields = relationshipName.split('\\.');

for (SObject record : this.recordsToTransform) {
String key = String.valueOf(record.get(keyField));
String value = extractNestedFieldValue(record, relationshipPathFields, targetKeyField);
Expand Down Expand Up @@ -2978,4 +3016,4 @@ public virtual inherited sharing class SOQL implements Queryable {
return Id.valueOf(prefix + '0000' + randomPart);
}
}
}
}
41 changes: 39 additions & 2 deletions force-app/main/default/classes/standard-soql/SOQL_Test.cls
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Copyright (c) 2025 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev)
* Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/soql-lib/blob/main/LICENSE)
*
* v6.1.0
* v6.2.0
*
* PMD False Positives:
* - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class
Expand Down Expand Up @@ -3522,6 +3522,43 @@ private class SOQL_Test {
Assert.isNotNull(result.Id, 'The result account Id should be always set even if the mocked account Id is not set.');
}

@IsTest
static void mockingQueryException() {
// Test
SOQL.mock('mockingQuery').throwException();

Exception queryException = null;

try {
Account result = (Account) SOQL.of(Account.SObjectType).with(Account.Name).mockId('mockingQuery').toObject();
} catch (QueryException e) {
queryException = e;
}

// Verify
Assert.isNotNull(queryException, 'The query exception should be thrown, because it\'s mocked.');
Assert.areEqual('List has no rows for assignment to SObject', queryException.getMessage(), 'The query exception should be thrown.');
}

@IsTest
static void mockingQueryExceptionWithMessage() {
// Test
String message = 'No such column \'Name\' on entity \'Account\'.';
SOQL.mock('mockingQuery').throwException(message);

Exception queryException = null;

try {
Account result = (Account) SOQL.of(Account.SObjectType).with(Account.Name).mockId('mockingQuery').toObject();
} catch (QueryException e) {
queryException = e;
}

// Verify
Assert.isNotNull(queryException, 'The query exception should be thrown, because it\'s mocked.');
Assert.areEqual(message, queryException.getMessage(), 'The query exception should be thrown.');
}

@IsTest
static void mockingCount() {
// Test
Expand Down Expand Up @@ -4475,7 +4512,7 @@ private class SOQL_Test {
static void mockedtoAggregatedIdMapByRelationshipField() {
// Setup
Id parentId = SOQL.IdGenerator.get(Account.SObjectType);

SOQL.mock('mockingQuery').thenReturn(new List<Account>{
new Account(Name = 'Account 1', Parent = new Account(Name = 'Parent 1', Id = parentId)),
new Account(Name = 'Account 2', Parent = new Account(Name = 'Parent 2', Id = parentId))
Expand Down
91 changes: 90 additions & 1 deletion website/docs/soql/advanced/mocking.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ public class ExampleControllerTest {

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

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

- `toIdsOf(SObjectField field)`
- `toIdsOf(String relationshipName, SObjectField field)`
Expand Down Expand Up @@ -425,3 +425,92 @@ public class ExampleControllerTest {
}
}
```

## Mocking Exceptions

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.

### Default Exception

The `.throwException()` method simulates a standard query exception with the default message: **"List has no rows for assignment to SObject"**.

This is useful for testing code paths that handle empty query results or unavailable data.

```apex title="ExampleController.cls"
public with sharing class ExampleController {
public static Account getAccountById(Id accountId) {
try {
return (Account) SOQL.of(Account.SObjectType)
.with(Account.Name, Account.BillingCity)
.byId(accountId)
.mockId('ExampleController.getAccountById')
.toObject();
} catch (Exception e) {
// Logger here
throw e;
}
}
}
```

```apex title="ExampleControllerTest.cls"
@IsTest
static void getAccountByIdException() {
SOQL.mock('ExampleController.getAccountById').throwException();

Test.startTest();
Exception error;
try {
Account result = ExampleController.getAccountById('001000000000000AAA');
} catch (Exception e) {
error = e;
}
Test.stopTest();

Assert.isNotNull(error, 'The query exception should be thrown.');
}
```

### Custom Exception Message

Use `.throwException(message)` to simulate a query exception with a custom error message, such as field-level security errors or invalid field references.

```apex title="Controller with Error Handling"
public with sharing class ExampleController {
public static Account getAccountById(Id accountId) {
try {
return (Account) SOQL.of(Account.SObjectType)
.with(Account.Name, Account.BillingCity)
.byId(accountId)
.mockId('ExampleController.getAccountById')
.toObject();
} catch (Exception e) {
// Logger here
throw e;
}
}
}
```

```apex title="Unit Test with Default Exception Mock"
@IsTest
public class ExampleControllerTest {
@IsTest
static void getAccountByIdException() {
String errorMessage = 'No such column \'InvalidField__c\' on entity \'Account\'.';
SOQL.mock('ExampleController.getAccountById').throwException(errorMessage);

Test.startTest();
Exception error;
try {
Account result = ExampleController.getAccountById('001000000000000AAA');
} catch (Exception e) {
error = e;
}
Test.stopTest();

Assert.isNotNull(error, 'The query exception should be thrown.');
}
}
```

Loading