From 252a6ef361d470724e626d98cfbf7886860f6585 Mon Sep 17 00:00:00 2001 From: Jan Sledziewski Date: Tue, 22 Aug 2023 22:42:03 +0200 Subject: [PATCH 1/7] Mocking initial version --- force-app/main/default/classes/SOSL.cls | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/force-app/main/default/classes/SOSL.cls b/force-app/main/default/classes/SOSL.cls index a194518..2dacd22 100644 --- a/force-app/main/default/classes/SOSL.cls +++ b/force-app/main/default/classes/SOSL.cls @@ -67,6 +67,8 @@ public inherited sharing class SOSL implements ISearchable { // RESULT List> toSearchList(); Search.SearchResults toSearchResult(); + // MOCKING + ISearchable mockId(String id); } public interface IReturning { @@ -152,6 +154,7 @@ public inherited sharing class SOSL implements ISearchable { private SearchableBuilder builder; private Executor executor; + private Mock mock; public static SOSL find(String searchText) { return new SOSL(searchText); @@ -160,6 +163,7 @@ public inherited sharing class SOSL implements ISearchable { private SOSL(String searchValue) { builder = new SearchableBuilder(searchValue); executor = new Executor(); + mock = new Mock(); } public SOSL inAllFields() { @@ -252,6 +256,11 @@ public inherited sharing class SOSL implements ISearchable { return this; } + public SOSL mockId(String mockId){ + executor.mockId = mockId; + return this; + } + public ISearchable systemMode() { executor.systemMode(); return this; @@ -267,6 +276,12 @@ public inherited sharing class SOSL implements ISearchable { return this; } + @TestVisible + private static void setMock(String mockId, List> results){ + mock.setMock(mockId, results); + } + + public override String toString() { return builder.toString(); } @@ -1083,10 +1098,18 @@ public inherited sharing class SOSL implements ISearchable { } public List> search(String query) { + if(mock.hasMock(mockId)){ + return mock.getMock(mockId); + } + return sharingExecutor.search(query, accessMode); } public Search.SearchResults find(String query) { + if(mock.hasMock(mockId)){ + return mock.getMock(mockId); + } + return sharingExecutor.find(query, accessMode); } } @@ -1126,6 +1149,22 @@ public inherited sharing class SOSL implements ISearchable { } } + private class Mock { + private final Map>> sObjectsMocks = new Map>>(); + + public void setMock(String mockId, List> results){ + sObjectsMocks.put(mockId, results); + } + + public Boolean hasMock(String mockId){ + return sObjectsMocks.containsKey(mockId); + } + + public List> getMock(String mockId){ + return sObjectsMocks.get(mockId); + } + } + private static String quote(String value) { return '\'' + value + '\''; } From e968ccf7e9a368f8411348050389856473cb76a6 Mon Sep 17 00:00:00 2001 From: Jan Sledziewski Date: Mon, 4 Sep 2023 20:36:05 +0200 Subject: [PATCH 2/7] Mocking with documentation --- force-app/main/default/classes/SOSL.cls | 9 +- force-app/main/default/classes/SOSL_Test.cls | 21 ++ website/docs/api/sosl.md | 27 +-- website/docs/docs/basic-features.md | 24 +- website/docs/docs/design.md | 2 + website/docs/docs/getting-started.md | 1 - website/docs/examples/mocking.md | 236 ++----------------- 7 files changed, 67 insertions(+), 253 deletions(-) diff --git a/force-app/main/default/classes/SOSL.cls b/force-app/main/default/classes/SOSL.cls index 2dacd22..c65984b 100644 --- a/force-app/main/default/classes/SOSL.cls +++ b/force-app/main/default/classes/SOSL.cls @@ -68,7 +68,7 @@ public inherited sharing class SOSL implements ISearchable { List> toSearchList(); Search.SearchResults toSearchResult(); // MOCKING - ISearchable mockId(String id); + ISearchable mockId(String queryIdentifier); } public interface IReturning { @@ -154,7 +154,7 @@ public inherited sharing class SOSL implements ISearchable { private SearchableBuilder builder; private Executor executor; - private Mock mock; + private static Mock mock = new Mock(); public static SOSL find(String searchText) { return new SOSL(searchText); @@ -163,7 +163,6 @@ public inherited sharing class SOSL implements ISearchable { private SOSL(String searchValue) { builder = new SearchableBuilder(searchValue); executor = new Executor(); - mock = new Mock(); } public SOSL inAllFields() { @@ -1106,10 +1105,6 @@ public inherited sharing class SOSL implements ISearchable { } public Search.SearchResults find(String query) { - if(mock.hasMock(mockId)){ - return mock.getMock(mockId); - } - return sharingExecutor.find(query, accessMode); } } diff --git a/force-app/main/default/classes/SOSL_Test.cls b/force-app/main/default/classes/SOSL_Test.cls index c48cc17..784596c 100644 --- a/force-app/main/default/classes/SOSL_Test.cls +++ b/force-app/main/default/classes/SOSL_Test.cls @@ -1210,4 +1210,25 @@ private class SOSL_Test { ) .preview(); } + + @IsTest + static void mock(){ + List testAccounts = new List{ new Account(Name = 'Mock') }; + SOSL.setMock('MockingExample', new List>{ testAccounts }); + + List> results = SOSL.find(SEARCH_TEXT) + .inAllFields() + .returning( + SOSL.returning(Account.SObjectType) + ) + .mockId('MockingExample') + .preview() + .toSearchList(); + + Assert.isFalse(results.isEmpty(), 'Should return results for at least one SObject'); + Assert.isFalse(results.get(0).isEmpty(), 'Accounts list shouldnt be empty'); + Assert.isNotNull(results.get(0).get(0), 'Account should be returned'); + Assert.areEqual(((Account) testAccounts.get(0)).Name, ((Account) results.get(0).get(0)).Name, 'Accounts name should be equal'); + + } } diff --git a/website/docs/api/sosl.md b/website/docs/api/sosl.md index 4b864b5..6f18f3f 100644 --- a/website/docs/api/sosl.md +++ b/website/docs/api/sosl.md @@ -589,31 +589,28 @@ SOSL.find('MySearch') ### mockId -TBD +Query needs unique id that allows for mocking. **Signature** ```apex -SOSL mockId(String queryIdentifier) +SOQL mockId(String queryIdentifier) ``` **Example** ```apex -TBD -``` -### list mock - -**Signature** - -```apex -SOSL setMock(String mockId, List records) -``` - -**Example** +SOSL.find(SEARCH_TEXT) + .inAllFields() + .returning( + SOSL.returning(Account.SObjectType) + ) + .mockId('MockingExample') + .toSearchList(); -```apex -TBD +// In Unit Test +List testAccounts = new List{ new Account(Name = 'Mock') }; +SOSL.setMock('MockingExample', new List>{ testAccounts }); ``` ## DEBUGGING diff --git a/website/docs/docs/basic-features.md b/website/docs/docs/basic-features.md index 06bb02e..fca986d 100644 --- a/website/docs/docs/basic-features.md +++ b/website/docs/docs/basic-features.md @@ -92,26 +92,26 @@ SOSL.of(Account.SObjectType) ## Mocking -Mocking provides a way to substitute records from a Database with some prepared data. Data can be prepared in form of SObject records and lists in Apex code or Static Resource `.csv` file. -Mocked queries won't make any SOSL's and simply return data set in method definition, mock __will ignore all filters and relations__, what is returned depends __solely on data provided to the method__. Mocking is working __only during test execution__. To mock SOSL query, use `.mockId(id)` method to make it identifiable. If you mark more than one query with the same ID, all marked queries will return the same data. +Mocking provides a way to substitute records from a Database search with some prepared data. Data can be prepared in form of list of `List`, in this format you can set multiple SObject types that will be passed to SOSL class. +Mocked queries won't make any SOSL's and simply return data set in method definition, mock __will ignore all filters and relations__, what is returned depends __solely on data provided to the method__. Mocking is working __only during test execution__. To mock SOSL query, use `.mockId(id)` method to make it identifiable. If you mark more than one query with the same ID, all marked queries will return the same data. Currently you can mock only `search` (`.toSearchList`) queries. Because `.find` (`.toSearchResult`) returns `Search.SearchResult` object which cannot be constructed in apex. ```apex public with sharing class ExampleController { - public static List getPartnerAccounts(String accountName) { - return SOQL_Account.query() - .with(Account.BillingCity, Account.BillingCountry) - .whereAre(SOSL.FilterGroup - .add(SOSL.Filter.name().contains(accountName)) - .add(SOSL.Filter.recordType().equal('Partner')) + public static List searchAccountsByName(String accountName) { + return SOSL.find(accountName) + .inAllFields() + .returning( + SOSL.returning(Account.SObjectType) ) - .mockId('ExampleController.getPartnerAccounts') - .toList(); - } + .mockId('MockingExample') + .toSearchList() + .get(0); + } } ``` -Then in test simply pass data you want to get from Selector to `SOSL.setMock(id, data)` method. Acceptable formats are: `List` or `SObject`. Then during execution Selector will return desired data. +Then in test simply pass data you want to get from Selector to `SOSL.setMock(id, data)` method. Acceptable format is: `List>`. Then during execution Selector will return desired data. ### List of records diff --git a/website/docs/docs/design.md b/website/docs/docs/design.md index 0427091..eb736d8 100644 --- a/website/docs/docs/design.md +++ b/website/docs/docs/design.md @@ -68,6 +68,8 @@ public interface ISearchable { // RESULT List> toSearchList(); Search.SearchResults toSearchResult(); + // MOCKING + ISearchable mockId(String queryIdentifier); } public interface IReturning { diff --git a/website/docs/docs/getting-started.md b/website/docs/docs/getting-started.md index 309d1fe..683950a 100644 --- a/website/docs/docs/getting-started.md +++ b/website/docs/docs/getting-started.md @@ -58,7 +58,6 @@ Read about the features in the [documentation](https://sosl.beyondthecloud.dev/d - 3.2 **without sharing** - 3.3 **inherited sharing** 4. **Mocking** -- 4.1 **Mock list of records** 5. **Dynamic conditions** ---- diff --git a/website/docs/examples/mocking.md b/website/docs/examples/mocking.md index aab7e11..8b7bffa 100644 --- a/website/docs/examples/mocking.md +++ b/website/docs/examples/mocking.md @@ -5,29 +5,33 @@ sidebar_position: 13 # MOCKING Mock SOSL results in Unit Tests. +Currently you can mock only `search` (`.toSearchList`) queries. Because `.find` (`.toSearchResult`) returns `Search.SearchResult` object which cannot be constructed in apex. You need to mock external objects. > In Apex tests, use dynamic SOSL to query external objects. Tests that perform static SOSL queries of external objects fail. ~ Salesforce -## Mock Single Record +## Mock Search results -Set mocking ID in Query declaration. +Firstly, set mocking ID in Query declaration. ```apex public with sharing class ExampleController { - public static List getAccountByName(String accountName) { - return SOQL_Account.query() - .with(Account.BillingCity, Account.BillingCountry) - .whereAre(SOSL.Filter.name().contains(accountName)) - .mockId('ExampleController.getAccountByName') - .toObject(); - } + public static List searchAccountsByName(String accountName) { + return SOSL.find(accountName) + .inAllFields() + .returning( + SOSL.returning(Account.SObjectType) + ) + .mockId('MockingExample') + .toSearchList() + .get(0); + } } ``` -Pass single SObject record to SOSL class, and use mock ID to target query to be mocked. +Pass list of `List` to SOSL class, and use mock Id to target query to be mocked. ```apex @IsTest @@ -36,220 +40,16 @@ public class ExampleControllerTest { @IsTest static void getAccountByName() { - SOSL.setMock('ExampleController.getAccountByName', new Account(Name = TEST_ACCOUNT_NAME)); + List testAccounts = new List{ new Account(Name = TEST_ACCOUNT_NAME) }; + SOSL.setMock('MockingExample', new List>{ testAccounts }); Test.startTest(); - Account result = (Account) ExampleController.getAccountByName(TEST_ACCOUNT_NAME); + Account result = (Account) ExampleController.searchAccountsByName(TEST_ACCOUNT_NAME); Test.stopTest(); - Assert.areEqual(TEST_ACCOUNT_NAME, result.Name); + Assert.areEqual(TEST_ACCOUNT_NAME, result.get(0).Name); } } ``` During execution Selector will return record that was set by `.setMock` method. - -## Mock Multiple Records - -Set mocking ID in Query declaration. - -```apex -public with sharing class ExampleController { - - public static List getPartnerAccounts(String accountName) { - return SOQL_Account.query() - .with(Account.BillingCity, Account.BillingCountry) - .whereAre(SOSL.FilterGroup - .add(SOSL.Filter.name().contains(accountName)) - .add(SOSL.Filter.recordType().equal('Partner')) - ) - .mockId('ExampleController.getPartnerAccounts') - .toList(); - } -} -``` -Pass List of SObject records to SOSL class, and use mock ID to target query to be mocked. - -```apex -@IsTest -public class ExampleControllerTest { - - @IsTest - static void getPartnerAccounts() { - List accounts = new List{ - new Account(Name = 'MyAccount 1'), - new Account(Name = 'MyAccount 2') - }; - - SOSL.setMock('ExampleController.getPartnerAccounts', accounts); - - Test.startTest(); - List result = ExampleController.getPartnerAccounts('MyAccount'); - Test.stopTest(); - - Assert.areEqual(accounts, result); - } -} -``` - -During execution Selector will return List of records that was set by `.setMock` method. - -## Mock Count Result - -Set mocking ID in Query declaration. - -```apex -public with sharing class ExampleController { - - public static List getPartnerAccountsCount(String accountName) { - return SOQL_Account.query() - .count() - .whereAre(SOSL.FilterGroup - .add(SOSL.Filter.name().contains(accountName)) - .add(SOSL.Filter.recordType().equal('Partner')) - ) - .mockId('ExampleController.getPartnerAccountsCount') - .toInteger(); - } -} -``` -Pass Integer value to SOSL class, and use mock ID to target query to be mocked. - -```apex -@IsTest -public class ExampleControllerTest { - private static final Integer TEST_VALUE = 5; - - @IsTest - static void getPartnerAccountsCount() { - SOSL.setMock('ExampleController.getPartnerAccountsCount', TEST_VALUE); - - Test.startTest(); - Integer result = ExampleController.getPartnerAccounts('MyAccount'); - Test.stopTest(); - - Assert.areEqual(TEST_VALUE, result); - } -} -``` - -During execution Selector will return Integer count that was set by `.setMock` method. - -## Mock with Static Resource - -Set mocking ID in Query declaration. - -```apex -public with sharing class ExampleController { - - public static List getPartnerAccounts(String accountName) { - return SOQL_Account.query() - .with(Account.BillingCity, Account.BillingCountry) - .whereAre(SOSL.FilterGroup - .add(SOSL.Filter.name().contains(accountName)) - .add(SOSL.Filter.recordType().equal('Partner')) - ) - .mockId('ExampleController.getPartnerAccounts') - .toList(); - } -} -``` - -Pass String value with name of Static Resource file with `.csv` records, and use mock ID to target query to be mocked. - -```apex -@IsTest -public class ExampleControllerTest { - - @IsTest - static void getPartnerAccounts() { - SOSL.setMock('ExampleController.getPartnerAccounts', Test.loadData(Account.SObjectType, 'MyAccounts')); - - Test.startTest(); - List result = ExampleController.getPartnerAccounts('MyAccount'); - Test.stopTest(); - - Assert.isNotNull(result); - } -} -``` - -During execution Selector will return records from Static Resource that was set by `.setMock` method. - -## Mock Sub-Query - -Set mocking ID in Query declaration. - -``` -public without sharing class AccountsController { - public static List getAccountsWithContacts() { - return SOSL.of(Account.SObjectType) - .with(Account.Name) - .with( - SOSL.SubQuery.of('Contacts') - .with(Contact.Id, Contact.Name, Contact.AccountId, Contact.Email) - ) - .mockId('AccountsController.getAccountsWithContacts') - .toList(); - } -} -``` - -Deserialize desired data from JSON format to selected SObjectType. And pass data in form of single record or list of records. - -``` -@IsTest -static void getAccountsWithContacts() { - List mocks = (List) JSON.deserialize( - '[{ "Name": "Account Name", "Contacts": { "totalSize": 1, "done": true, "records": [{ "Name": "Contact Name", "Email": "contact.email@address.com" }] } }], - List.class - ); - - List accounts; - - Test.startTest(); - SOSL.setMock('AccountsController.getAccountsWithContacts', mocks); - accounts = AccountsController.getAccountsWithContacts(); - Test.stopTest(); - - Assert.isNotNull(accounts); - Assert.isNotNull(accounts[0].contacts); - Assert.areEqual(1, accounts[0].contacts.size()); -} -``` - -Or create data with Test Data Factory and Serialize/Deserialize it to use as a Mock. - -``` -@IsTest -static void getAccountsWithContacts() { - List mocks = (List) JSON.deserialize( - JSON.serialize( - new List>{ - new Map{ - 'Name' => 'Account Name', - 'Contacts' => new Map{ - 'totalSize' => 1, - 'done' => true, - 'records' => new List{ new Contact(FirstName = 'Contact', LastName = 'Name', Email = 'contact.email@address.com') } - } - } - } - ), - List.class - ); - - List accounts; - - Test.startTest(); - SOSL.setMock('AccountsController.getAccountsWithContacts', mocks); - accounts = AccountsController.getAccountsWithContacts(); - Test.stopTest(); - - Assert.isNotNull(accounts); - Assert.isNotNull(accounts[0].contacts); - Assert.areEqual(1, accounts[0].contacts.size()); -} -``` - -During execution Selector will ignore filters and return data set by a mock. From 2416e9372ff2cbf8d35c11faaf2b9dfbd4c98fae Mon Sep 17 00:00:00 2001 From: Jan Sledziewski Date: Sun, 1 Oct 2023 21:15:35 +0200 Subject: [PATCH 3/7] mocking update --- force-app/main/default/classes/SOSL.cls | 50 ++++++++++++++------ force-app/main/default/classes/SOSL_Test.cls | 35 ++++++++++++-- 2 files changed, 67 insertions(+), 18 deletions(-) diff --git a/force-app/main/default/classes/SOSL.cls b/force-app/main/default/classes/SOSL.cls index c65984b..4a8ec4a 100644 --- a/force-app/main/default/classes/SOSL.cls +++ b/force-app/main/default/classes/SOSL.cls @@ -255,7 +255,7 @@ public inherited sharing class SOSL implements ISearchable { return this; } - public SOSL mockId(String mockId){ + public SOSL mockId(String mockId) { executor.mockId = mockId; return this; } @@ -276,10 +276,14 @@ public inherited sharing class SOSL implements ISearchable { } @TestVisible - private static void setMock(String mockId, List> results){ + private static void setMock(String mockId, List> results) { mock.setMock(mockId, results); } + @TestVisible + private static void setMock(String mockId, List results) { + mock.setMock(mockId, results); + } public override String toString() { return builder.toString(); @@ -383,23 +387,23 @@ public inherited sharing class SOSL implements ISearchable { private class SoslSearchClause implements QueryClause { private String searchGroup; - public void inAllFields(){ + public void inAllFields() { this.searchGroup = 'ALL'; } - public void inEmailFields(){ + public void inEmailFields() { this.searchGroup = 'EMAIL'; } - public void inNameFields(){ + public void inNameFields() { this.searchGroup = 'NAME'; } - public void inPhoneFields(){ + public void inPhoneFields() { this.searchGroup = 'PHONE'; } - public void inSidebarFields(){ + public void inSidebarFields() { this.searchGroup = 'SIDEBAR'; } @@ -742,7 +746,7 @@ public inherited sharing class SOSL implements ISearchable { } public void withNetworkIn(Iterable networkIds) { - add('NETWORK IN (\'' + String.join(networkIds, '\', \'') + '\')'); + add('NETWORK IN (\'' + String.join(networkIds, '\', \'') + '\')'); } public void withPriceBookId(Id priceBookId) { @@ -773,7 +777,6 @@ public inherited sharing class SOSL implements ISearchable { public class SoslUpdateClauses implements QueryClause { private List updateClauses = new List(); - public void updateViewStat() { updateClauses.add('VIEWSTAT'); } @@ -971,7 +974,7 @@ public inherited sharing class SOSL implements ISearchable { } public IFilter equal(String value) { - return set('=', quote(value)); + return set('=', quote(value)); } public IFilter notEqual(Object value) { @@ -1097,7 +1100,9 @@ public inherited sharing class SOSL implements ISearchable { } public List> search(String query) { - if(mock.hasMock(mockId)){ + if (mock.hasFixedMock(mockId)) { + Test.setFixedSearchResults(mock.getFixedMock(mockId)); + } else if (mock.hasMock(mockId)) { return mock.getMock(mockId); } @@ -1105,6 +1110,10 @@ public inherited sharing class SOSL implements ISearchable { } public Search.SearchResults find(String query) { + if (mock.hasFixedMock(mockId)) { + Test.setFixedSearchResults(mock.getFixedMock(mockId)); + } + return sharingExecutor.find(query, accessMode); } } @@ -1146,18 +1155,31 @@ public inherited sharing class SOSL implements ISearchable { private class Mock { private final Map>> sObjectsMocks = new Map>>(); + private final Map> fixedResultsMocks = new Map>(); - public void setMock(String mockId, List> results){ + public void setMock(String mockId, List> results) { sObjectsMocks.put(mockId, results); } - public Boolean hasMock(String mockId){ + public void setMock(String mockId, List results) { + fixedResultsMocks.put(mockId, results); + } + + public Boolean hasMock(String mockId) { return sObjectsMocks.containsKey(mockId); } - public List> getMock(String mockId){ + public Boolean hasFixedMock(String mockId) { + return fixedResultsMocks.containsKey(mockId); + } + + public List> getMock(String mockId) { return sObjectsMocks.get(mockId); } + + public List getFixedMock(String mockId) { + return fixedResultsMocks.get(mockId); + } } private static String quote(String value) { diff --git a/force-app/main/default/classes/SOSL_Test.cls b/force-app/main/default/classes/SOSL_Test.cls index 784596c..27cec79 100644 --- a/force-app/main/default/classes/SOSL_Test.cls +++ b/force-app/main/default/classes/SOSL_Test.cls @@ -1212,15 +1212,13 @@ private class SOSL_Test { } @IsTest - static void mock(){ + static void mock() { List testAccounts = new List{ new Account(Name = 'Mock') }; SOSL.setMock('MockingExample', new List>{ testAccounts }); List> results = SOSL.find(SEARCH_TEXT) .inAllFields() - .returning( - SOSL.returning(Account.SObjectType) - ) + .returning(SOSL.returning(Account.SObjectType)) .mockId('MockingExample') .preview() .toSearchList(); @@ -1229,6 +1227,35 @@ private class SOSL_Test { Assert.isFalse(results.get(0).isEmpty(), 'Accounts list shouldnt be empty'); Assert.isNotNull(results.get(0).get(0), 'Account should be returned'); Assert.areEqual(((Account) testAccounts.get(0)).Name, ((Account) results.get(0).get(0)).Name, 'Accounts name should be equal'); + } + + @IsTest + static void mockFixedIds() { + List testAccounts = new List{ new Account(Name = 'Mock') }; + insert testAccounts; + + List fixedIds = new List(); + for(Account account: testAccounts){ + fixedIds.add(account.Id); + } + + SOSL.setMock('MockingExample', fixedIds); + + List> results; + Test.startTest(); + results = SOSL.find(SEARCH_TEXT) + .inAllFields() + .returning(SOSL.returning(Account.SObjectType)) + .mockId('MockingExample') + .preview() + .toSearchList(); + Test.stopTest(); + List fixedAccounts = [SELECT Name FROM Account]; + + Assert.isFalse(results.isEmpty(), 'Should return results for at least one SObject'); + Assert.isFalse(results.get(0).isEmpty(), 'Accounts list shouldnt be empty'); + Assert.isNotNull(results.get(0).get(0), 'Account should be returned'); + Assert.areEqual(((Account) fixedAccounts.get(0)).Name, ((Account) results.get(0).get(0)).Name, 'Accounts name should be equal'); } } From 33d68b90a5b62efa876db1e6878546d469b294f9 Mon Sep 17 00:00:00 2001 From: Jan Sledziewski Date: Tue, 10 Oct 2023 22:04:19 +0200 Subject: [PATCH 4/7] fix tests --- force-app/main/default/classes/SOSL_Test.cls | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/force-app/main/default/classes/SOSL_Test.cls b/force-app/main/default/classes/SOSL_Test.cls index 27cec79..6d743f0 100644 --- a/force-app/main/default/classes/SOSL_Test.cls +++ b/force-app/main/default/classes/SOSL_Test.cls @@ -1213,6 +1213,7 @@ private class SOSL_Test { @IsTest static void mock() { + // Test List testAccounts = new List{ new Account(Name = 'Mock') }; SOSL.setMock('MockingExample', new List>{ testAccounts }); @@ -1223,6 +1224,7 @@ private class SOSL_Test { .preview() .toSearchList(); + // Verify Assert.isFalse(results.isEmpty(), 'Should return results for at least one SObject'); Assert.isFalse(results.get(0).isEmpty(), 'Accounts list shouldnt be empty'); Assert.isNotNull(results.get(0).get(0), 'Account should be returned'); @@ -1231,28 +1233,24 @@ private class SOSL_Test { @IsTest static void mockFixedIds() { - List testAccounts = new List{ new Account(Name = 'Mock') }; + // Test + List testAccounts = new List{ new Account(Name = SEARCH_TEXT) }; insert testAccounts; - List fixedIds = new List(); - for(Account account: testAccounts){ - fixedIds.add(account.Id); - } - - SOSL.setMock('MockingExample', fixedIds); + SOSL.setMock('MockingExample', new List{ testAccounts.get(0).Id }); List> results; Test.startTest(); results = SOSL.find(SEARCH_TEXT) .inAllFields() - .returning(SOSL.returning(Account.SObjectType)) + .returning(SOSL.returning(Account.SObjectType).with(Account.Name)) .mockId('MockingExample') .preview() .toSearchList(); Test.stopTest(); + // Verify List fixedAccounts = [SELECT Name FROM Account]; - Assert.isFalse(results.isEmpty(), 'Should return results for at least one SObject'); Assert.isFalse(results.get(0).isEmpty(), 'Accounts list shouldnt be empty'); Assert.isNotNull(results.get(0).get(0), 'Account should be returned'); From 85685d237dc60f930b614a0998b37da173502847 Mon Sep 17 00:00:00 2001 From: Jan Sledziewski Date: Sun, 15 Oct 2023 19:36:48 +0200 Subject: [PATCH 5/7] improve documentation --- website/docs/api/sosl.md | 2 +- website/docs/docs/basic-features.md | 13 +++- website/docs/examples/mocking.md | 110 +++++++++++++++++++++++++--- 3 files changed, 110 insertions(+), 15 deletions(-) diff --git a/website/docs/api/sosl.md b/website/docs/api/sosl.md index 6f18f3f..6716fe6 100644 --- a/website/docs/api/sosl.md +++ b/website/docs/api/sosl.md @@ -594,7 +594,7 @@ Query needs unique id that allows for mocking. **Signature** ```apex -SOQL mockId(String queryIdentifier) +SOSL mockId(String queryIdentifier) ``` **Example** diff --git a/website/docs/docs/basic-features.md b/website/docs/docs/basic-features.md index fca986d..17b1b93 100644 --- a/website/docs/docs/basic-features.md +++ b/website/docs/docs/basic-features.md @@ -92,8 +92,11 @@ SOSL.of(Account.SObjectType) ## Mocking -Mocking provides a way to substitute records from a Database search with some prepared data. Data can be prepared in form of list of `List`, in this format you can set multiple SObject types that will be passed to SOSL class. -Mocked queries won't make any SOSL's and simply return data set in method definition, mock __will ignore all filters and relations__, what is returned depends __solely on data provided to the method__. Mocking is working __only during test execution__. To mock SOSL query, use `.mockId(id)` method to make it identifiable. If you mark more than one query with the same ID, all marked queries will return the same data. Currently you can mock only `search` (`.toSearchList`) queries. Because `.find` (`.toSearchResult`) returns `Search.SearchResult` object which cannot be constructed in apex. +Mocking provides a way to substitute records from a Database search with some prepared data. Data can be prepared in form of list of lists (similar to SOSL return) `List>`, in this format you can set multiple SObject types that will be passed to SOSL class. Or you can provide fixed ids, as to out of the box SOSL in form of `List`. + +In case of preset results SOSL won't make any SOSL's queries and simply return data set in method definition, mock __will ignore all filters and relations__, what is returned depends __solely on data provided to the method__. Mocking is working __only during test execution__. If fixed Ids are provided to SOSL class it will use standard `Test.setFixedSearchResults` method, to make sure expected results are returned. + +To mock SOSL query, use `.mockId(id)` method to make it identifiable. If you mark more than one query with the same ID, all marked queries will return the same data. ```apex public with sharing class ExampleController { @@ -111,7 +114,11 @@ public with sharing class ExampleController { } ``` -Then in test simply pass data you want to get from Selector to `SOSL.setMock(id, data)` method. Acceptable format is: `List>`. Then during execution Selector will return desired data. +If you want to use `search` and don't want to insert any records to database, you can simply set mock data with `SOSL.setMock(String mockId, List> data)` method. + +If you need to use `find` or it doesn't matter if records are in database, you can set fixed results by providing records ids with `SOSL.setMock(String mockId, List ids)` method. + +Then during execution Selector will return desired data. ### List of records diff --git a/website/docs/examples/mocking.md b/website/docs/examples/mocking.md index 8b7bffa..0eaf7f8 100644 --- a/website/docs/examples/mocking.md +++ b/website/docs/examples/mocking.md @@ -4,18 +4,15 @@ sidebar_position: 13 # MOCKING -Mock SOSL results in Unit Tests. -Currently you can mock only `search` (`.toSearchList`) queries. Because `.find` (`.toSearchResult`) returns `Search.SearchResult` object which cannot be constructed in apex. - -You need to mock external objects. +Mock SOSL results in Unit Tests. External objects require mocking. > In Apex tests, use dynamic SOSL to query external objects. Tests that perform static SOSL queries of external objects fail. ~ Salesforce -## Mock Search results - -Firstly, set mocking ID in Query declaration. +## Mock without Database insert +Mocking without inserting records to database is only posible using `search` (`.toSearchList`) method. This is caused by return type for `find` which is returning `Search.SearchResults` which cannot be constructed. -```apex +1. Set Mock Id in Query declaration +```java public with sharing class ExampleController { public static List searchAccountsByName(String accountName) { @@ -31,9 +28,20 @@ public with sharing class ExampleController { } ``` -Pass list of `List` to SOSL class, and use mock Id to target query to be mocked. +1. Pass list of `List` to SOSL class, and using `setMock(String mockId, List> records)` method. Set `mockId` to target query to be mocked. +```java +List testAccounts = new List{ new Account(Name = TEST_ACCOUNT_NAME) }; +SOSL.setMock('MockingExample', new List>{ testAccounts }); +``` + + +3. During execution Selector will return record that was set by `.setMock` method. +```java +Assert.areEqual(TEST_ACCOUNT_NAME, result.get(0).Name); +``` -```apex +4. Full test method: +```java @IsTest public class ExampleControllerTest { private static final String TEST_ACCOUNT_NAME = 'MyAccount 1'; @@ -52,4 +60,84 @@ public class ExampleControllerTest { } ``` -During execution Selector will return record that was set by `.setMock` method. +## Mock with database insert +You can mock both `search` and `find` after inserting records into database. + +1. Set Mock Id in Query declaration +```java +public with sharing class ExampleController { + + public static List searchAccountsByName(String accountName) { + return SOSL.find(accountName) + .inAllFields() + .returning( + SOSL.returning(Account.SObjectType) + ) + .mockId('MockingExample') + .toSearchList() + .get(0); + } +} +``` + +2. Inserts records to database and pass `List` to SOSL class using `setMock(String mockId, List) records` method. Remember to specify `mockId` to target selected query. +```java +List testAccounts = new List{ new Account(Name = SEARCH_TEXT) }; +insert testAccounts; + +SOSL.setMock('MockingExample', new List{ testAccounts.get(0).Id }); +``` + +3. During execution Selector will return record that was set by `.setMock` method. It will use standard `Test.setFixedSearchResults` method before return statement. + +SOSL Class: +```java +if (mock.hasFixedMock(mockId)) { + Test.setFixedSearchResults(mock.getFixedMock(mockId)); +} +``` + +Return in test: +```java + List> results = SOSL.find(SEARCH_TEXT) + .inAllFields() + .returning(SOSL.returning(Account.SObjectType).with(Account.Name)) + .mockId('MockingExample') + .preview() + .toSearchList(); + +List fixedAccounts = [SELECT Name FROM Account]; +Assert.isFalse(results.isEmpty(), 'Should return results for at least one SObject'); +Assert.isFalse(results.get(0).isEmpty(), 'Accounts list shouldnt be empty'); +Assert.isNotNull(results.get(0).get(0), 'Account should be returned'); +Assert.areEqual(((Account) fixedAccounts.get(0)).Name, ((Account) results.get(0).get(0)).Name, 'Accounts name should be equal'); +``` + +4. Full test method: +```java +@IsTest +static void mockFixedIds() { + // Test + List testAccounts = new List{ new Account(Name = SEARCH_TEXT) }; + insert testAccounts; + + SOSL.setMock('MockingExample', new List{ testAccounts.get(0).Id }); + + List> results; + Test.startTest(); + results = SOSL.find(SEARCH_TEXT) + .inAllFields() + .returning(SOSL.returning(Account.SObjectType).with(Account.Name)) + .mockId('MockingExample') + .preview() + .toSearchList(); + Test.stopTest(); + + // Verify + List fixedAccounts = [SELECT Name FROM Account]; + Assert.isFalse(results.isEmpty(), 'Should return results for at least one SObject'); + Assert.isFalse(results.get(0).isEmpty(), 'Accounts list shouldnt be empty'); + Assert.isNotNull(results.get(0).get(0), 'Account should be returned'); + Assert.areEqual(((Account) fixedAccounts.get(0)).Name, ((Account) results.get(0).get(0)).Name, 'Accounts name should be equal'); +} +``` From a2c9085c65d4a73804c3d66eae74769e365851c2 Mon Sep 17 00:00:00 2001 From: Jan Sledziewski Date: Thu, 19 Oct 2023 22:56:33 +0200 Subject: [PATCH 6/7] Cover Search.find method with mocking --- force-app/main/default/classes/SOSL_Test.cls | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/force-app/main/default/classes/SOSL_Test.cls b/force-app/main/default/classes/SOSL_Test.cls index dae087e..de06d62 100644 --- a/force-app/main/default/classes/SOSL_Test.cls +++ b/force-app/main/default/classes/SOSL_Test.cls @@ -1316,4 +1316,29 @@ private class SOSL_Test { Assert.isNotNull(results.get(0).get(0), 'Account should be returned'); Assert.areEqual(((Account) fixedAccounts.get(0)).Name, ((Account) results.get(0).get(0)).Name, 'Accounts name should be equal'); } + + @IsTest + static void mockFindFixedIds() { + // Test + List testAccounts = new List{ new Account(Name = SEARCH_TEXT) }; + insert testAccounts; + + SOSL.setMock('MockingExample', new List{ testAccounts.get(0).Id }); + + Search.SearchResults results; + Test.startTest(); + results = SOSL.find(SEARCH_TEXT) + .inAllFields() + .returning(SOSL.returning(Account.SObjectType).with(Account.Name)) + .mockId('MockingExample') + .preview() + .toSearchResult(); + Test.stopTest(); + + // Verify + List fixedAccounts = [SELECT Name FROM Account]; + Assert.isFalse(results.get('Account').isEmpty(), 'Accounts list shouldnt be empty'); + Assert.isNotNull(results.get('Account').get(0), 'Account should be returned'); + Assert.areEqual(((Account) fixedAccounts.get(0)).Name, ((Account) results.get('Account').get(0).getSObject()).Name, 'Accounts name should be equal'); + } } From 9521230984e785e04477d2b32dbc917ca9794a27 Mon Sep 17 00:00:00 2001 From: Jan Sledziewski Date: Thu, 19 Oct 2023 23:25:44 +0200 Subject: [PATCH 7/7] Fix String binding test --- force-app/main/default/classes/SOSL.cls | 1 + force-app/main/default/classes/SOSL_Test.cls | 30 ++++++++++---------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/force-app/main/default/classes/SOSL.cls b/force-app/main/default/classes/SOSL.cls index f5292a3..d712380 100644 --- a/force-app/main/default/classes/SOSL.cls +++ b/force-app/main/default/classes/SOSL.cls @@ -122,6 +122,7 @@ public inherited sharing class SOSL implements ISearchable { IFilter isNotNull(); IFilter isTrue(); IFilter isFalse(); + IFilter equal(String value); IFilter equal(Object value); IFilter notEqual(Object value); IFilter lessThan(Object value); diff --git a/force-app/main/default/classes/SOSL_Test.cls b/force-app/main/default/classes/SOSL_Test.cls index de06d62..186dd10 100644 --- a/force-app/main/default/classes/SOSL_Test.cls +++ b/force-app/main/default/classes/SOSL_Test.cls @@ -219,7 +219,7 @@ private class SOSL_Test { // Verify Assert.areEqual('FIND \'SearchText\' IN ALL FIELDS RETURNING Account(Id, Name WHERE RecordType.DeveloperName = :binded0)', sosl.toString()); - Assert.areEqual('Partner', sosl.binding()[0]); + Assert.areEqual('\'Partner\'', sosl.binding()[0]); } @IsTest @@ -235,7 +235,7 @@ private class SOSL_Test { // Verify Assert.areEqual('FIND \'SearchText\' IN ALL FIELDS RETURNING Account(Id, Name WHERE Name = :binded0)', sosl.toString()); - Assert.areEqual('Test', sosl.binding()[0]); + Assert.areEqual('\'Test\'', sosl.binding()[0]); } @IsTest @@ -251,7 +251,7 @@ private class SOSL_Test { // Verify Assert.areEqual('FIND \'SearchText\' IN ALL FIELDS RETURNING Account(Id, Name WHERE Industry = :binded0)', sosl.toString()); - Assert.areEqual('IT', sosl.binding()[0]); + Assert.areEqual('\'IT\'', sosl.binding()[0]); } @IsTest @@ -267,7 +267,7 @@ private class SOSL_Test { // Verify Assert.areEqual('FIND \'SearchText\' IN ALL FIELDS RETURNING Contact(Id, Name WHERE Account.Industry = :binded0)', sosl.toString()); - Assert.areEqual('IT', sosl.binding()[0]); + Assert.areEqual('\'IT\'', sosl.binding()[0]); } @IsTest @@ -283,7 +283,7 @@ private class SOSL_Test { // Verify Assert.areEqual('FIND \'SearchText\' IN ALL FIELDS RETURNING Account(Id, Name WHERE Industry = :binded0)', sosl.toString()); - Assert.areEqual('IT', sosl.binding()[0]); + Assert.areEqual('\'IT\'', sosl.binding()[0]); } @IsTest @@ -379,7 +379,7 @@ private class SOSL_Test { // Verify Assert.areEqual('FIND \'SearchText\' IN ALL FIELDS RETURNING Account(Id, Name WHERE Name = :binded0)', sosl.toString()); - Assert.areEqual('My Account', sosl.binding()[0]); + Assert.areEqual('\'My Account\'', sosl.binding()[0]); } @IsTest @@ -700,7 +700,7 @@ private class SOSL_Test { // Verify Assert.areEqual('FIND \'SearchText\' IN ALL FIELDS RETURNING Account(Id, Name WHERE (Industry = :binded0))', sosl.toString()); - Assert.areEqual('IT', sosl.binding()[0]); + Assert.areEqual('\'IT\'', sosl.binding()[0]); } @IsTest @@ -721,8 +721,8 @@ private class SOSL_Test { Assert.areEqual('FIND \'SearchText\' IN ALL FIELDS RETURNING Account(Id, Name WHERE (Name = :binded0 AND Industry = :binded1))', sosl.toString()); List bindedValues = sosl.binding(); - Assert.areEqual('MyAccount', bindedValues[0]); - Assert.areEqual('IT', bindedValues[1]); + Assert.areEqual('\'MyAccount\'', bindedValues[0]); + Assert.areEqual('\'IT\'', bindedValues[1]); } @IsTest @@ -744,8 +744,8 @@ private class SOSL_Test { Assert.areEqual('FIND \'SearchText\' IN ALL FIELDS RETURNING Account(Id, Name WHERE (Name = :binded0 OR Industry = :binded1))', sosl.toString()); List bindedValues = sosl.binding(); - Assert.areEqual('MyAccount', bindedValues[0]); - Assert.areEqual('IT', bindedValues[1]); + Assert.areEqual('\'MyAccount\'', bindedValues[0]); + Assert.areEqual('\'IT\'', bindedValues[1]); } @IsTest @@ -771,8 +771,8 @@ private class SOSL_Test { Assert.areEqual('FIND \'SearchText\' IN ALL FIELDS RETURNING Account(Id, Name WHERE (Name = :binded0 AND Industry = :binded1 AND (NumberOfEmployees > :binded2 OR IsDeleted = :binded3)))', sosl.toString()); List bindedValues = sosl.binding(); - Assert.areEqual('MyAccount', bindedValues[0]); - Assert.areEqual('IT', bindedValues[1]); + Assert.areEqual('\'MyAccount\'', bindedValues[0]); + Assert.areEqual('\'IT\'', bindedValues[1]); Assert.areEqual(5, bindedValues[2]); Assert.areEqual(false, bindedValues[3]); } @@ -813,8 +813,8 @@ private class SOSL_Test { Assert.areEqual('FIND \'SearchText\' IN ALL FIELDS RETURNING Account(Id, Name WHERE (Name = :binded0 OR Industry = :binded1))', sosl.toString()); List bindedValues = sosl.binding(); - Assert.areEqual('MyAccount', bindedValues[0]); - Assert.areEqual('IT', bindedValues[1]); + Assert.areEqual('\'MyAccount\'', bindedValues[0]); + Assert.areEqual('\'IT\'', bindedValues[1]); } @isTest