diff --git a/force-app/main/default/classes/cached-soql/SOQLCache.cls b/force-app/main/default/classes/cached-soql/SOQLCache.cls index b223f00a..d27c8a0e 100644 --- a/force-app/main/default/classes/cached-soql/SOQLCache.cls +++ b/force-app/main/default/classes/cached-soql/SOQLCache.cls @@ -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 diff --git a/force-app/main/default/classes/cached-soql/SOQLCache_Test.cls b/force-app/main/default/classes/cached-soql/SOQLCache_Test.cls index 8014f239..c5b0620c 100644 --- a/force-app/main/default/classes/cached-soql/SOQLCache_Test.cls +++ b/force-app/main/default/classes/cached-soql/SOQLCache_Test.cls @@ -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 diff --git a/force-app/main/default/classes/soql-evaluator/SOQLEvaluator.cls b/force-app/main/default/classes/soql-evaluator/SOQLEvaluator.cls index 17d52481..02337165 100644 --- a/force-app/main/default/classes/soql-evaluator/SOQLEvaluator.cls +++ b/force-app/main/default/classes/soql-evaluator/SOQLEvaluator.cls @@ -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 diff --git a/force-app/main/default/classes/soql-evaluator/SOQLEvaluator_Test.cls b/force-app/main/default/classes/soql-evaluator/SOQLEvaluator_Test.cls index 5d229d75..0bcdc9a2 100644 --- a/force-app/main/default/classes/soql-evaluator/SOQLEvaluator_Test.cls +++ b/force-app/main/default/classes/soql-evaluator/SOQLEvaluator_Test.cls @@ -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 diff --git a/force-app/main/default/classes/standard-soql/SOQL.cls b/force-app/main/default/classes/standard-soql/SOQL.cls index 61b53b35..580ec112 100644 --- a/force-app/main/default/classes/standard-soql/SOQL.cls +++ b/force-app/main/default/classes/standard-soql/SOQL.cls @@ -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 @@ -417,6 +417,9 @@ public virtual inherited sharing class SOQL implements Queryable { Mockable thenReturn(Map aggregatedResult); // Count Mockable thenReturn(Integer count); + // Exception + void throwException(); + void throwException(String message); } // Backward support - it's going to be removed in the future @@ -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 toMap(SObjectField keyField) { this.with(keyField); this.whereAre(Filter.with(keyField).isNotNull()); @@ -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> aggregatedResults) { this.aggregateResultMock.add(aggregatedResults); @@ -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; @@ -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; @@ -2619,6 +2648,7 @@ public virtual inherited sharing class SOQL implements Queryable { this.incrementQueryIssued(); if (!this.mocks.isEmpty()) { + this.throwExceptionMockIfExists(); return this.getMockedListProxy(); } @@ -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 getMockedListProxy() { if (this.mocks.size() == 1) { return this.mocks[0].sObjectMock.get(this.builder.fields, this.builder.subQueries); @@ -2643,6 +2679,7 @@ public virtual inherited sharing class SOQL implements Queryable { this.incrementQueryIssued(); if (!this.mocks.isEmpty()) { + this.throwExceptionMockIfExists(); return this.getMockedAggregateProxy(); } @@ -2660,6 +2697,7 @@ public virtual inherited sharing class SOQL implements Queryable { this.incrementQueryIssued(); if (!this.mocks.isEmpty()) { + this.throwExceptionMockIfExists(); return this.getMockedCount(); } @@ -2930,7 +2968,7 @@ public virtual inherited sharing class SOQL implements Queryable { public Map> toAggregatedMap(SObjectField keyField, String relationshipName, SObjectField targetKeyField) { Map> customValuesPerCustomKey = new Map>(); List relationshipPathFields = relationshipName.split('\\.'); - + for (SObject record : this.recordsToTransform) { String key = String.valueOf(record.get(keyField)); String value = extractNestedFieldValue(record, relationshipPathFields, targetKeyField); @@ -2978,4 +3016,4 @@ public virtual inherited sharing class SOQL implements Queryable { return Id.valueOf(prefix + '0000' + randomPart); } } -} \ No newline at end of file +} diff --git a/force-app/main/default/classes/standard-soql/SOQL_Test.cls b/force-app/main/default/classes/standard-soql/SOQL_Test.cls index f3c9900c..d64302bb 100644 --- a/force-app/main/default/classes/standard-soql/SOQL_Test.cls +++ b/force-app/main/default/classes/standard-soql/SOQL_Test.cls @@ -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 @@ -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 @@ -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{ 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)) diff --git a/website/docs/soql/advanced/mocking.md b/website/docs/soql/advanced/mocking.md index 3c0d9ba5..9e4c7899 100644 --- a/website/docs/soql/advanced/mocking.md +++ b/website/docs/soql/advanced/mocking.md @@ -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 -It’s 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)` @@ -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.'); + } +} +``` + diff --git a/website/docs/soql/api/soql.md b/website/docs/soql/api/soql.md index 1948866c..de2f44e0 100644 --- a/website/docs/soql/api/soql.md +++ b/website/docs/soql/api/soql.md @@ -173,6 +173,8 @@ The following are methods for using `SOQL`: - [`SOQL.mock(String mockId).thenReturn(SObject record)`](#record-mock) - [`SOQL.mock(String mockId).thenReturn(List records)`](#record-mock) - [`SOQL.mock(String mockId).thenReturn(Integer amount)`](#count-mock) +- [`SOQL.mock(String mockId).throwException()`](#exception-mock) +- [`SOQL.mock(String mockId).throwException(String message)`](#exception-mock-with-message) [**DEBUGGING**](#debugging) @@ -2203,6 +2205,68 @@ List result = SOQL.of(Lead.SObjectType) .toAggregatedProxy(); ``` +### exception mock + +Mock a query exception with default message. + +**Signature** + +```apex +SOQL.Mockable mock(String mockId).throwException() +``` + +**Example** + +```apex +// In Unit Test +SOQL.mock('MyQuery').throwException(); + +Test.startTest(); +Exception error; +try { + Account result = SOQL.of(Account.SObjectType) + .mockId('MyQuery') + .toObject(); +} catch (Exception e) { + error = e; +} +Test.stopTest(); + +Assert.isNotNull(error, 'The query exception should be thrown.'); +``` + +### exception mock with message + +Mock a query exception with custom error message. + +**Signature** + +```apex +SOQL.Mockable mock(String mockId).throwException(String message) +``` + +**Example** + +```apex +// In Unit Test +String errorMessage = 'No such column \'InvalidField__c\' on entity \'Account\'.'; +SOQL.mock('MyQuery').throwException(errorMessage); + +Test.startTest(); +Exception error; +try { + Account result = SOQL.of(Account.SObjectType) + .mockId('MyQuery') + .toObject(); +} catch (Exception e) { + error = e; +} +Test.stopTest(); + +Assert.isNotNull(error, 'The query exception should be thrown.'); +Assert.isTrue(error.getMessage().contains('InvalidField__c')); +``` + ## DEBUGGING ### preview diff --git a/website/docs/soql/examples/mocking.md b/website/docs/soql/examples/mocking.md index b52cf2eb..cdc8bc74 100644 --- a/website/docs/soql/examples/mocking.md +++ b/website/docs/soql/examples/mocking.md @@ -364,4 +364,90 @@ private class ExampleControllerTest { Assert.areEqual(2, result); } } +``` + +## Mocking Exceptions + +SOQL Lib provides the ability to mock query exceptions, which is essential for testing error handling logic in your code. + +### Default Exception + +Use `.throwException()` to simulate a standard query exception with the default message: "List has no rows for assignment to SObject". + +```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 +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.'); + } +} ``` \ No newline at end of file