diff --git a/fflib/src/classes/fflib_DomainObjectBuilder.cls b/fflib/src/classes/fflib_DomainObjectBuilder.cls new file mode 100644 index 00000000000..617e6373999 --- /dev/null +++ b/fflib/src/classes/fflib_DomainObjectBuilder.cls @@ -0,0 +1,450 @@ +/** + * Copyright (c) 2014, FinancialForce.com, inc + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the FinancialForce.com, inc nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ + +/** + * Base class aiding in the implementation of Test Data Builder pattern as described by Nate Pryce + * (http://www.natpryce.com/). Derived classes can combine the features the Builder + * Pattern (http://www.c2.com/cgi/wiki?BuilderPattern) with an Object Mother pattern + * (http://www.c2.com/cgi/wiki?ObjectMother) to simplify the setup/creation of data. + * + * While test data in unit tests should be as simple as possible and could even be mocked with frameworks like ApexMock, + * complex enterprise software also needs integration tests. The TestDataBuilder pattern reduces complexity, + * eliminates redundancy and improves readability while setting up complex test data structures for both + * unit and integration tests and can also be used for production purposes. + * + * Learn how to replace your redundant, error-prone and test-specific helper classes with a set of small domain classes + * and how to write more readable tests by checking the samples in the test class fflib_DomainObjectBuilderTest + */ +public abstract class fflib_DomainObjectBuilder +{ + /** + * @description Maintains non-relationship field values that have been assigned to an instance of this class + **/ + protected Map m_fieldValueMap = new Map(); + + /** + * @description Maintains relationship field values that have been assigned to an instance of this class + * Relationship field values are references to builder instances + **/ + protected Map m_parentByRelationship = new Map(); + + /** + * @description The SObjectType this Builder class represents + **/ + protected Schema.SObjectType m_sObjType; + + /** + * @description The SObject this Builder class is building + **/ + protected SObject m_sObjRecord { get; protected set; } + + /** + * @description Tracks all builders that have registered to be persisted to a UnitOfWork + **/ + private static Set m_registeredBuilders = new Set(); + + /** + * @description Constructs the Builder class with the specified SObjectType + * + * @param type The SObject type that the builder will build + **/ + protected fflib_DomainObjectBuilder(SObjectType type) { + this.m_sObjType = type; + // passing false because of bug in SFDC - See https://success.salesforce.com/issues_view?id=a1p30000000Sz5RAAS + // not establishing defaults also gives us an opportunity for a completely "clean" record + this.m_sObjRecord = m_sObjType.newSObject(null, false); + this.IsBuilt = false; + this.IsRegistered = false; + } + + /** + * @description Copy Constructor that constructs the Builder class based on the builder specified + * + * @param copyFrom The builder to copy/clone this instance from + **/ + protected fflib_DomainObjectBuilder(fflib_DomainObjectBuilder copyFrom) + { + this(copyFrom.getSObjectType()); + m_fieldValueMap = copyFrom.m_fieldValueMap.clone(); + m_parentByRelationship = copyFrom.m_parentByRelationship.clone(); + } + + /** + * @description Commits all registered builders and their related builders to the Unit Of Work specified + * + * @param uow The Unit Of Work to process against + **/ + public static void persistRegistered(fflib_ISObjectUnitOfWork uow) { + persist(uow, m_registeredBuilders); + } + + /** + * @description Returns true if builder has been built, false otherwise + * A builder IsBuilt build or persist operations have completed successfully on it + **/ + public Boolean IsBuilt { get; protected set; } + + /** + * @description Returns true if builder has been registered, false otherwise + * A builder IsRegistered if registerBuilder has been called on it and the + * builder has not yet been built/persisted + **/ + public Boolean IsRegistered { get; protected set; } + + /** + * @description Returns the SObject associated to this builder + **/ + public SObject getRecord() { + return m_sObjRecord; + } + + /** + * @description Returns the SObjectType associated to this builder + **/ + public virtual Schema.SObjectType getSObjectType() { + return m_sObjType; + } + + /** + * @description Returns the SObject after building the builder and any of its related builders + * + * @param isNew True if the instance should not have an Id value (new record), false otherwise (existing record) + * + * @remarks Recommended to wrap this method with a 'build()' and 'buildNew()' in a derived class + * casting to the derived type to support fluent configuration + **/ + public virtual SObject build(Boolean isNew) + { + // see if we'll let 'em through + checkAllowBuild(); + + // fire event + beforeBuild(isNew); + + // set non-relationship field values + for (Schema.SObjectField field : this.m_fieldValueMap.keySet()) { + m_sObjRecord.put(field, this.m_fieldValueMap.get(field)); + } + + // set relationship field values + for(Schema.SObjectField rel: this.m_parentByRelationship.keySet()) { + // get the related builder + fflib_DomainObjectBuilder parent = this.m_parentByRelationship.get(rel); + + // if not built yet, we must, we must + if (!parent.IsBuilt) { + // should not be building a builder that is registered + if (parent.IsRegistered) { + throw new DomainObjectBuilderException(String.format('Field {0} contains value for a builder that is marked for registration', new List { rel.getDescribe().getName() })); + } + + // related object should always be trated as existing + parent.build(false); + // we must have an Id value for all relationship fields - if Id is Null builder + // was built using isNew = true or Id has been cleared manually + } else if (null == parent.m_sObjRecord.Id) { + throw new DomainObjectBuilderException(String.format('Field {0} contains value for a builder that was built as new', new List { rel.getDescribe().getName() })); + } + + // set field value with related SObject Id + m_sObjRecord.put(rel, parent.m_sObjRecord.Id); + } + + // establish Id value + m_sObjRecord.Id = isNew ? null : fflib_IDGenerator.generate(getSObjectType()); + + // update state of builder + markAsBuilt(); + + // fire event + afterBuild(m_sObjRecord); + + // let 'em have it + return m_sObjRecord; + } + + /** + * @description Commits this builder and its related builders to the Unit Of Work specified + * + * @param uow The Unit Of Work to process against + * + * @remarks Recommended to wrap this method with a 'persist' method in a derived class + * casting to the derived type to support fluent configuration + **/ + public virtual SObject persistBuilder(fflib_ISObjectUnitOfWork uow) + { + // persist this builder + persist(uow, new Set { this }); + + // let 'em have it + return m_sObjRecord; + } + + /** + * @description Registers builder to be processed by persistRegistered + * + * Returns itself to support fluent configuration + * + * @remarks Recommended to wrap this method with a 'register()' method in a derived class + * casting to the derived type to support fluent configuration + **/ + protected virtual fflib_DomainObjectBuilder registerBuilder() { + // see if we'll let 'em through + checkAllowRegister(); + + // add to registered list + m_registeredBuilders.add(this); + + // mark as registered + markAsRegistered(); + + // return the builder itself + return this; + } + + /** + * @description Validates that builder is not registered + * + * @throws DomainObjectBuilderException if builder is already registered + **/ + protected virtual void checkIsRegistered() { + if (IsRegistered) { + throw new DomainObjectBuilderException('Builder has already been registered'); + } + } + + /** + * @description Validates that builder is not built + * + * @throws DomainObjectBuilderException if builder is already built + **/ + protected virtual void checkIsBuilt() { + if (IsBuilt) { + throw new DomainObjectBuilderException('Builder has already been built'); + } + } + + /** + * @description Validates that builder can be built + * + * @throws DomainObjectBuilderException if builder is registered or is already built + **/ + protected virtual void checkAllowBuild() + { + // should not allow build if builder is registered + checkIsRegistered(); + + // should not allow build if already built + checkIsBuilt(); + } + + /** + * @description Validates that builder can be registered + * + * @throws DomainObjectBuilderException if builder is registered or is already built + **/ + protected virtual void checkAllowRegister() + { + // should not register a builder that has already been registered + checkIsRegistered(); + + // should not register a builder that has been built + checkIsBuilt(); + } + + /** + * @description Validates that builder can be changed + * + * @throws DomainObjectBuilderException if builder is built + **/ + protected virtual void checkAllowSet() + { + // should not change data on builder after its built + checkIsBuilt(); + } + + /** + * @description This method allows subclasses to invoke any action before the SObject is built. + * + * @param isNew True if building as new, false otherwise (existing) + **/ + protected virtual void beforeBuild(Boolean isNew) {} + + /** + * @description This method allows subclasses to handle the SObject after it is built. + * + * @param sObj The SObject that has been built. + **/ + protected virtual void afterBuild(SObject sObj) {} + + /** + * @description This method allows subclasses to handle the SObject before it is inserted + * + * @param sObj The SObject that has been built. + **/ + protected virtual void beforeInsert(SObject sObj) {} + + /** + * @description This method allows subclasses to handle the SObject after it is inserted. + * + * @param sObj The SObject that has been built. + **/ + protected virtual void afterInsert(SObject sObj) {} + + /** + * @description Updates the builder state to indicate it has been built + **/ + protected virtual void markAsBuilt() + { + // mark as built + IsBuilt = true; + // clear registered flag (might or might not have been registered depending on which 'build/persist' process was used + IsRegistered = false; + } + + /** + * @description Updates the builder state to indicate it has been registered + **/ + protected virtual void markAsRegistered() + { + // mark as registered + IsRegistered = true; + } + + /** + * @description Update the builder field with the value specified + * + * @param field The SObjectField to set the value on + * @param value The value for the SObjectField + **/ + protected virtual fflib_DomainObjectBuilder set(Schema.SObjectField field, Object value) { + // should not allow changing data after built + checkAllowSet(); + + // set field value + m_fieldValueMap.put(field, value); + + // right back at ya + return this; + } + + /** + * @description Updates the builder field with the value specified + * + * @param parentRelationship The SObjectField to set the value on + * @param parent The builder instance for the SObjectField + **/ + protected virtual fflib_DomainObjectBuilder setParent(Schema.SObjectField parentRelationship, fflib_DomainObjectBuilder parent) { + // should not allow changing data after built + checkAllowSet(); + + // Note: The parent registered last always wins! + m_parentByRelationship.put(parentRelationship, parent); + + // give and you shall receive + return this; + } + + /** + * @description Commits the set of builders specified and their related builders to the Unit Of Work specified + * + * @param uow The Unit Of Work to process against + * @param builders The builders to persist to the Unit Of Work + **/ + private static void persist(fflib_ISObjectUnitOfWork uow, Set builders) { + // track what we've prepared + Set preparedBuilders = new Set(); + + // iterate builders processing each one + for (fflib_DomainObjectBuilder builder :builders) { + prepareForCommit(uow, builder, preparedBuilders); + } + + // commit work + uow.commitWork(); + + // iterate all builders marking them built + for (fflib_DomainObjectBuilder builder :preparedBuilders) { + // mark as built + builder.markAsBuilt(); + + // fire event + builder.afterInsert(builder.m_sObjRecord); + } + + // reset builders Set + builders.clear(); + } + + /** + * @description Prepares a builder to be committed through a Unit Of Work + * + * @param uow The Unit Of Work to process against + * @param builder The builder to prepare + * @param preparedBuilders The builders that have already been prepared during the current operation + **/ + private static Set prepareForCommit(fflib_ISObjectUnitOfWork uow, fflib_DomainObjectBuilder builder, Set preparedBuilders) { + // fire event + builder.beforeInsert(builder.m_sObjRecord); + + // set non-relationship field values + for (Schema.SObjectField field : builder.m_fieldValueMap.keySet()) { + builder.m_sObjRecord.put(field, builder.m_fieldValueMap.get(field)); + } + + // set relationship field values + for(Schema.SObjectField rel: builder.m_parentByRelationship.keySet()) { + // get related builder + fflib_DomainObjectBuilder parent = builder.m_parentByRelationship.get(rel); + + // if its not in the registered list, we must manually add it to be persisted + if (!parent.IsRegistered) { + // cannot persist a builder that has been built + if (parent.IsBuilt) { + throw new DomainObjectBuilderException(String.format('Field {0} contains value for a builder that has already been built', new List { rel.getDescribe().getName() })); + } + if (!preparedBuilders.contains(parent)) { + prepareForCommit(uow, parent, preparedBuilders); + } + } + + uow.registerRelationship(builder.m_sObjRecord, rel, parent.m_sObjRecord); + } + + // register as new + uow.registerNew(builder.m_sObjRecord); + + // we've completed the preparation + preparedBuilders.add(builder); + + return preparedBuilders; + } + + /** + * General exception class for builders + **/ + public class DomainObjectBuilderException extends Exception {} +} \ No newline at end of file diff --git a/fflib/src/classes/fflib_DomainObjectBuilder.cls-meta.xml b/fflib/src/classes/fflib_DomainObjectBuilder.cls-meta.xml new file mode 100644 index 00000000000..b12420ea0e7 --- /dev/null +++ b/fflib/src/classes/fflib_DomainObjectBuilder.cls-meta.xml @@ -0,0 +1,5 @@ + + + 31.0 + Active + diff --git a/fflib/src/classes/fflib_DomainObjectBuilderTest.cls b/fflib/src/classes/fflib_DomainObjectBuilderTest.cls new file mode 100644 index 00000000000..4937270e8c2 --- /dev/null +++ b/fflib/src/classes/fflib_DomainObjectBuilderTest.cls @@ -0,0 +1,1779 @@ +/** + * Copyright (c) 2014, FinancialForce.com, inc + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the FinancialForce.com, inc nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +**/ +@IsTest +private class fflib_DomainObjectBuilderTest +{ + /** + * @description Flag passed to helper methods to determine which test to run + **/ + private enum MethodMode { Build, BuildNew, Persist } + + /** + * @description UnitOfWork SObjectTypes used for persist methods + **/ + private static List UOW_SOBJECTTYPES = + new Schema.SObjectType[] { + Account.SObjectType, + Contact.SObjectType, + Product2.SObjectType, + PriceBook2.SObjectType, + PriceBookEntry.SObjectType, + Opportunity.SObjectType, + OpportunityLineItem.SObjectType }; + + private static void runBuild(Boolean buildNew) + { + // Given + TestAccountBuilder acctBuilder = anAccount(); + Account beforeBuildRecord = acctBuilder.Record; // SObject available on builder instantiation + + // When + Account acct = buildNew ? acctBuilder.buildNew() : acctBuilder.build(); + + // Then + System.assertEquals(true, acctBuilder.IsBuilt); + System.assertEquals(false, acctBuilder.IsRegistered); + System.assertNotEquals(null, acct); + System.assertNotEquals(null, acctBuilder.Record); + System.assertEquals(beforeBuildRecord, acctBuilder.Record); // should be same SObject + System.assertEquals(acct, acctBuilder.Record); // should be same SObject + if (buildNew) { + System.assertEquals(null, acctBuilder.Record.Id); + System.assertEquals(null, acct.Id); + } else { + System.assertNotEquals(null, acctBuilder.Record.Id); + System.assertNotEquals(null, acct.Id); + } + System.assertEquals(2, acctBuilder.getDomainObjectBuilderEventsFired().size()); + System.assertEquals(buildNew, acctBuilder.getDomainObjectBuilderEventsFired().get('beforeBuild')); + System.assertEquals(acct, acctBuilder.getDomainObjectBuilderEventsFired().get('afterBuild')); + } + + private static void runBuildTwice(Boolean buildNewFirstCall, Boolean buildNewSecondCall) + { + // Given + TestAccountBuilder acctBuilder = anAccount(); + Account acct = buildNewFirstCall ? acctBuilder.buildNew() : acctBuilder.build(); + + // When + fflib_DomainObjectBuilder.DomainObjectBuilderException caughtEx; + try { + Account acct2 = buildNewSecondCall ? acctBuilder.buildNew() : acctBuilder.build(); + } catch (fflib_DomainObjectBuilder.DomainObjectBuilderException ex) { + caughtEx = ex; + } + + // Then + System.assertNotEquals(null, caughtEx, 'Expected exception'); + System.assertEquals('Builder has already been built', caughtEx.getMessage()); + } + + private static void runBuildWithValues(Boolean buildNew) + { + // Given + TestAccountBuilder acctBuilder = anAccount().withName('My Account'); + TestContactBuilder contactBuilder = aContact().withLastName('My Contact').withAccount(acctBuilder); + + // When + Contact contact = buildNew ? contactBuilder.buildNew() : contactBuilder.build(); + + // Then + System.assertNotEquals(null, acctBuilder.Record.Id); + if (buildNew) { + System.assertEquals(null, contactBuilder.Record.Id); + } else { + System.assertNotEquals(null, contactBuilder.Record.Id); + } + System.assertEquals(contact.LastName, 'My Contact'); + System.assertEquals(contactBuilder.Record.LastName, 'My Contact'); + System.assertEquals(acctBuilder.Record.Name, 'My Account'); + System.assertEquals(contact.AccountId, acctBuilder.Record.Id); + + } + + private static void runBuildAfterRegistering(Boolean buildNew) + { + // Given + TestAccountBuilder acctBuilder = anAccount(); + acctBuilder.register(); + + // When + fflib_DomainObjectBuilder.DomainObjectBuilderException caughtEx; + try { + Account acct = buildNew ? acctBuilder.buildNew() : acctBuilder.build(); + } catch (fflib_DomainObjectBuilder.DomainObjectBuilderException ex) { + caughtEx = ex; + } + + // Then + System.assertNotEquals(null, caughtEx, 'Expected exception'); + System.assertEquals('Builder has already been registered', caughtEx.getMessage()); + } + + private static void runBuildWithRelationshipThatWasBuiltNew(Boolean buildNew) + { + // Given + TestAccountBuilder accountBuilder = anAccount(); + Account account = accountBuilder.buildNew(); + TestContactBuilder testContactBuilder = aContact().withAccount(accountBuilder); + + // When + fflib_DomainObjectBuilder.DomainObjectBuilderException caughtEx; + try { + Contact contact = buildNew ? testContactBuilder.buildNew() : testContactBuilder.build(); + } catch (fflib_DomainObjectBuilder.DomainObjectBuilderException ex) { + caughtEx = ex; + } + + // Then + System.assertNotEquals(null, caughtEx, 'Expected exception'); + System.assertEquals(String.format('Field {0} contains value for a builder that was built as new', new List { Contact.AccountId.getDescribe().getName() }), caughtEx.getMessage()); + } + + private static void runRegisterAfterBuilding(Boolean buildNew) + { + // Given + TestAccountBuilder acctBuilder = anAccount(); + Account acct = buildNew ? acctBuilder.buildNew() : acctBuilder.build(); + + // When + fflib_DomainObjectBuilder.DomainObjectBuilderException caughtEx; + try { + acctBuilder.register(); + } catch (fflib_DomainObjectBuilder.DomainObjectBuilderException ex) { + caughtEx = ex; + } + + // Then + System.assertNotEquals(null, caughtEx, 'Expected exception'); + System.assertEquals('Builder has already been built', caughtEx.getMessage()); + } + + private static void runBuildWithRegisteredRelationship(Boolean buildNew) + { + // Given + TestAccountBuilder acctBuilder = anAccount().register(); + TestContactBuilder contactBuilder = aContact().withAccount(acctBuilder); + + // When + fflib_DomainObjectBuilder.DomainObjectBuilderException caughtEx; + try { + Contact contact = buildNew ? contactBuilder.buildNew() : contactBuilder.build(); + } catch (fflib_DomainObjectBuilder.DomainObjectBuilderException ex) { + caughtEx = ex; + } + + // Then + System.assertNotEquals(null, caughtEx, 'Expected exception'); + System.assertEquals(String.format('Field {0} contains value for a builder that is marked for registration', new List { Contact.AccountId.getDescribe().getName() }), caughtEx.getMessage()); + } + + private static void verifySetThrowsException(TestContactBuilder contactBuilder, Boolean useRelationshipField) + { + // When + fflib_DomainObjectBuilder.DomainObjectBuilderException caughtEx; + try { + TestContactBuilder builder = useRelationshipField ? contactBuilder.withAccount(anAccount()) : contactBuilder.withLastName('TestLastName'); + } catch (fflib_DomainObjectBuilder.DomainObjectBuilderException ex) { + caughtEx = ex; + } + + // Then + System.assertNotEquals(null, caughtEx, 'Expected exception'); + System.assertEquals('Builder has already been built', caughtEx.getMessage()); + } + + private static void runPersistWithRelationshipThatWasBuilt(MethodMode mode) + { + // Given + TestAccountBuilder accountBuilder = anAccount().withName('Test Account'); + if (MethodMode.Persist == mode) { + accountBuilder.register(); + fflib_DomainObjectBuilder.persistRegistered(new fflib_SObjectUnitOfWork(UOW_SOBJECTTYPES)); + } else { + Account account = (MethodMode.BuildNew == mode) ? accountBuilder.buildNew() : accountBuilder.build(); + } + TestContactBuilder testContactBuilder = aContact().withLastName('Test Contact').withAccount(accountBuilder).register(); + + // When + fflib_DomainObjectBuilder.DomainObjectBuilderException caughtEx; + try { + fflib_DomainObjectBuilder.persistRegistered(new fflib_SObjectUnitOfWork(UOW_SOBJECTTYPES)); + } catch (fflib_DomainObjectBuilder.DomainObjectBuilderException ex) { + caughtEx = ex; + } + + // Then + System.assertNotEquals(null, caughtEx, 'Expected exception'); + System.assertEquals(String.format('Field {0} contains value for a builder that has already been built', new List { Contact.AccountId.getDescribe().getName() }), caughtEx.getMessage()); + } + + private static void runBuildRelationshipTree(Boolean buildNew) + { + // Given + TestAccountBuilder accountBuilder1 = anAccount().withName('Test Account1'); + TestAccountBuilder accountBuilder2 = anAccount().withName('Test Account2'); + TestOpportunityBuilder opportunityBuilder1 = aClosedWonOpportunity().withAccount(accountBuilder2); + TestOpportunityBuilder opportunityBuilder2 = opportunityBuilder1.but().withName('Built with but').withAccount(accountBuilder1); + TestContactBuilder contactBuilder1 = aContact().withLastName('Jones').withAccount(accountBuilder1); + TestContactBuilder contactBuilder2 = contactBuilder1.but().withLastName('Smith'); + + // When + Opportunity opportunity1 = buildNew ? opportunityBuilder1.buildNew() : opportunityBuilder1.build(); + Opportunity opportunity2 = buildNew ? opportunityBuilder2.buildNew() : opportunityBuilder2.build(); + Contact contact1 = buildNew ? contactBuilder1.buildNew() : contactBuilder1.build(); + Contact contact2 = buildNew ? contactBuilder2.buildNew() : contactBuilder2.build(); + + // Then + Account account1 = accountBuilder1.Record; + Account account2 = accountBuilder2.Record; + verifyRelationshipTree(account1, account2, contact1, contact2, opportunity1, opportunity2, buildNew); + } + + private static SObject assertAndGet(Map records, Id searchForId) + { + System.assertNotEquals(null, searchForId); + System.assertEquals(true, records.containsKey(searchForId)); + return records.get(searchForId); + } + + private static void verifyRelationshipTree(Account account1, Account account2, Contact contact1, Contact contact2, Opportunity opportunity1, Opportunity opportunity2, Boolean buildNew) + { + System.assertNotEquals(null, account1.Id); + System.assertNotEquals(null, account2.Id); + System.assert(buildNew ? (null == opportunity1.Id) : (null != opportunity1.Id), 'Value for Opportunity1 Id was not expected'); + System.assert(buildNew ? (null == opportunity2.Id) : (null != opportunity2.Id), 'Value for Opportunity2 Id was not expected'); + System.assert(buildNew ? (opportunity1.Id == opportunity2.Id) : (opportunity1.Id != opportunity2.Id), 'Opportunity1 Id and Opportunity2 Id mismatch'); + System.assertEquals('Closed Won', opportunity1.StageName); + System.assertEquals('Built with but', opportunity2.Name); + System.assertNotEquals(opportunity1.Name, opportunity2.Name); + System.assertNotEquals(null, opportunity1.AccountId); + System.assertNotEquals(null, opportunity2.AccountId); + System.assertNotEquals(opportunity1.AccountId, opportunity2.AccountId); + System.assert(buildNew ? (null == contact1.Id) : (null != contact1.Id), 'Value for Contact1 Id was not expected'); + System.assert(buildNew ? (null == contact2.Id) : (null != contact2.Id), 'Value for Contact2 Id was not expected'); + System.assert(buildNew ? (contact1.Id == contact2.Id) : (contact1.Id != contact2.Id), 'Contact1 Id and Contact2 Id mismatch'); + System.assertEquals('Jones', contact1.LastName); + } + + private static void runDeepRelationshipTree(MethodMode mode) + { + // Given + TestProductBuilder prdBuilder = aProduct() + .withName('My Test Product'); + + TestPriceBookEntryBuilder pbeBuilder = aPriceBookEntryWithStandardPriceBook() + .withUnitPrice(200) + .withIsActive(true) + .withProduct(prdBuilder); + + TestOpportunityLineItemBuilder oliBuilder = anOpportunityLineItem() + .withQuantity(5) + .withTotalPrice(500) + .withPriceBookEntry( + aPriceBookEntry() + .withUnitPrice(100) + .withIsActive(true) + .withUseStandardPrice(false) + .withProduct(prdBuilder) + .withPriceBook( + aPriceBook() + .withName('My PriceBook') + .withIsActive(true))) + .withOpportunity( + anOpportunity() + .withName('My Test Opportunity') + .withStageName('Prospecting') + .withCloseDate(System.today()) + .withAccount( + anAccount() + .withName('My Test Account'))); + + if (MethodMode.Persist == mode) { + // When + pbeBuilder.register(); + oliBuilder.register(); + fflib_DomainObjectBuilder.persistRegistered(new fflib_SObjectUnitOfWork(UOW_SOBJECTTYPES)); + + // Then + System.assertEquals(1, [SELECT COUNT() FROM Opportunity]); + System.assertEquals(1, [SELECT COUNT() FROM OpportunityLineItem]); + System.assertEquals(1, [SELECT COUNT() FROM PriceBook2]); + System.assertEquals(2, [SELECT COUNT() FROM PriceBookEntry]); + System.assertEquals(1, [SELECT COUNT() FROM Product2]); + System.assertEquals(1, [SELECT COUNT() FROM Account]); + + } else { + // When + PriceBookEntry pbe = MethodMode.BuildNew == mode ? pbeBuilder.buildNew() : pbeBuilder.build(); + OpportunityLineItem oli = MethodMode.BuildNew == mode ? oliBuilder.buildNew() : oliBuilder.build(); + + // Then + System.assertNotEquals(null, prdBuilder.Record.Id); + System.assert(MethodMode.BuildNew == mode ? pbe.Id == null : pbe.Id != null, 'Value for PriceBookEntry Id was not expected'); + System.assertEquals(prdBuilder.Record.Id, pbe.Product2Id); + System.assert(MethodMode.BuildNew == mode ? oli.Id == null : null != oli.Id, 'Value for OpportunityLineItem Id was not expected'); + System.assertNotEquals(null, oli.OpportunityId); + System.assertNotEquals(null, oli.PriceBookEntryId); + } + } + + /** + * @description Confirms that creating a builder has the correct initial state + **/ + @IsTest + private static void testCreateBuilderInstance() + { + // Given + TestAccountBuilder acctBuilder = anAccount(); + + // When + + // Then + System.assertNotEquals(null, acctBuilder.Record); // does not require build to have SObject instance created + System.assertEquals(false, acctBuilder.IsBuilt); + System.assertEquals(false, acctBuilder.IsRegistered); + System.assertEquals(null, acctBuilder.Record.Id); + System.assertEquals(Account.SObjectType, acctBuilder.getSObjectType()); + } + + /** + * @description Confirms that calling build results with a valid SObject that has an Id + **/ + @IsTest + private static void testBuild() + { + runBuild(false); + } + + /** + * @description Confirms that calling buildNew results with a valid SObject that does NOT have an Id + **/ + @IsTest + private static void testBuildNew() + { + runBuild(true); + } + + /** + * @description Confirms that calling register results in the builder having the correct state + **/ + @IsTest + private static void testRegister() + { + // Given + TestAccountBuilder acctBuilder = anAccount(); + Account beforeBuildRecord = acctBuilder.Record; + + // When + acctBuilder.register(); + + // Then + System.assertEquals(false, acctBuilder.IsBuilt); + System.assertEquals(true, acctBuilder.IsRegistered); + System.assertNotEquals(null, acctBuilder.Record); + System.assertEquals(null, acctBuilder.Record.Id); + } + + /** + * @description Confirms that a builder cannot be built twice + **/ + @IsTest + private static void testBuildingTwiceThrowsException() + { + // build and build + runBuildTwice(false, false); + // build and buildNew + runBuildTwice(false, true); + // buildNew and buildNew + runBuildTwice(true, true); + // buildNew and build + runBuildTwice(true, false); + } + + /** + * @description Confirms that a builder cannot be built if it is registered + **/ + @IsTest + private static void testBuildAfterRegisteringThrowsException() + { + runBuildAfterRegistering(false); + } + + /** + * @description Confirms that a builder cannot be builtnew if it is registered + **/ + @IsTest + private static void testBuildNewAfterRegisteringThrowsException() + { + runBuildAfterRegistering(true); + runBuildAfterRegistering(false); + } + + /** + * @description Confirms that a builder cannot be registered if it has been built + **/ + @IsTest + private static void testRegisteringAfterBuildThrowsException() + { + runRegisterAfterBuilding(false); + runRegisterAfterBuilding(true); + } + + /** + * @description Confirms that a builder cannot be registered twice + **/ + @IsTest + private static void testRegisteringTwiceThrowsException() + { + // Given + TestAccountBuilder acctBuilder = anAccount(); + + // When + acctBuilder.register(); + + // Then + System.assertEquals(true, acctBuilder.IsRegistered); + System.assertEquals(false, acctBuilder.IsBuilt); + System.assertEquals(null, acctBuilder.Record.Id); + + // When + fflib_DomainObjectBuilder.DomainObjectBuilderException caughtEx; + try { + acctBuilder.register(); + } catch (fflib_DomainObjectBuilder.DomainObjectBuilderException ex) { + caughtEx = ex; + } + + // Then + System.assertNotEquals(null, caughtEx, 'Expected exception'); + System.assertEquals('Builder has already been registered', caughtEx.getMessage()); + } + + /** + * @description Confirms that a builder field data cannot be changed after it has been built + **/ + @IsTest + private static void testChangingDataAfterBuildThrowsException() + { + // Given + TestContactBuilder contactBuilder = aContact(); + Contact contact = contactBuilder.build(); + + // When and Then - Non relationship field + verifySetThrowsException(contactBuilder, false); + // When and Then - relationship field + verifySetThrowsException(contactBuilder, true); + } + + /** + * @description Confirms that a builder field data cannot be changed after it has been builtnew + **/ + @IsTest + private static void testChangingDataAfterBuildNewThrowsException() + { + // Given + TestContactBuilder contactBuilder = aContact(); + Contact contact = contactBuilder.buildNew(); + + // When and Then - Non relationship field + verifySetThrowsException(contactBuilder, false); + // When and Then - relationship field + verifySetThrowsException(contactBuilder, true); + } + + /** + * @description Confirms that a builder field data cannot be changed after it has been persisted + **/ + @IsTest + private static void testChangingDataAfterPersistThrowsException() + { + // Given + TestContactBuilder contactBuilder = aContact().withLastName('Test Contact').register(); + fflib_DomainObjectBuilder.persistRegistered(new fflib_SObjectUnitOfWork(UOW_SOBJECTTYPES)); + + // When and Then - Non relationship field + verifySetThrowsException(contactBuilder, false); + // When and Then - relationship field + verifySetThrowsException(contactBuilder, true); + } + + /** + * @description Confirms that a builder that contains a relationship field that references a builder + * that has been registered cannot be built + **/ + @IsTest + private static void testBuildWhenRelationshipIsRegisteredThrowsException() + { + // build + runBuildWithRegisteredRelationship(false); + // buildNew + runBuildWithRegisteredRelationship(true); + } + + /** + * @description Confirms that calling persistRegistered results with a valid SObject in the database + **/ + @IsTest + private static void testPersistRegistered() + { + // Given + Integer numAccounts = 5; + List builders = new List(); + for (Integer i = 0; i < numAccounts; i++) { + builders.add(anAccount().withName('Account' + i).register()); + } + + // When + fflib_DomainObjectBuilder.persistRegistered(new fflib_SObjectUnitOfWork(UOW_SOBJECTTYPES)); + + // Then + Map accounts = new Map([SELECT Id, Name FROM Account]); + System.assertEquals(builders.size(), accounts.size()); + for (Integer i = 0; i < numAccounts; i++) { + TestAccountBuilder builder = builders[i]; + System.assertEquals(true, builder.IsBuilt); + System.assertEquals(false, builder.IsRegistered); + System.assertNotEquals(null, builder.Record.Id); + System.assert(accounts.containsKey(builder.Record.Id), 'Builder Id ' + builder.Record.Id + ' not found in database'); + System.assertEquals(builder.Record, accounts.get(builder.Record.Id)); + System.assertEquals('Account' + i, accounts.get(builder.Record.Id).Name); + System.assertEquals(2, builder.getDomainObjectBuilderEventsFired().size()); + System.assertEquals(builder.Record, builder.getDomainObjectBuilderEventsFired().get('beforeInsert')); + System.assertEquals(builder.Record, builder.getDomainObjectBuilderEventsFired().get('afterInsert')); + } + } + + /** + * @description Confirms that building results in all fields containing correct values + **/ + @IsTest + private static void testBuildingWithValuesHasCorrectValues() + { + runBuildWithValues(false); + runBuildWithValues(true); + } + + /** + * @description Confirms that persisting results in all fields containing correct values + **/ + @IsTest + private static void testPersistingWithValuesHasCorrectValues() + { + // Given + Integer numAccounts = 5; + List accountBuilders = new List(); + List contactBuilders = new List(); + for (Integer i = 0; i < numAccounts; i++) { + TestAccountBuilder accountBuilder = anAccount().withName('My Account' + i).register(); + TestContactBuilder contactBuilder = aContact().withLastName('My Contact' + i).withAccount(accountBuilder).register(); + accountBuilders.add(accountBuilder); + contactBuilders.add(contactBuilder); + } + + // When + fflib_DomainObjectBuilder.persistRegistered(new fflib_SObjectUnitOfWork(UOW_SOBJECTTYPES)); + + // Then + Map accounts = new Map([SELECT Id, Name FROM Account]); + Map contacts = new Map([SELECT Id, LastName, AccountId FROM Contact]); + System.assertEquals(numAccounts, accounts.size()); + System.assertEquals(numAccounts, contacts.size()); + for (Integer i = 0; i < numAccounts; i++) { + TestAccountBuilder accountBuilder = accountBuilders[i]; + TestContactBuilder contactBuilder = contactBuilders[i]; + Account account = accountBuilder.Record; + Contact contact = contactBuilder.Record; + + System.assertEquals(true, accounts.containsKey(account.Id)); + System.assertEquals(account, accounts.get(account.Id)); + System.assertEquals('My Account' + i, accounts.get(account.Id).Name); + System.assertEquals(true, contacts.containsKey(contact.Id)); + System.assertEquals(contact, contacts.get(contact.Id)); + System.assertEquals('My Contact' + i, contacts.get(contact.Id).LastName); + System.assertEquals(account.Id, contact.AccountId); + } + } + + /** + * @description Confirms that copy constructor (clone) results in a builder instance + * with the same exact field values as the source instance + **/ + @IsTest + private static void testClone() + { + // Given + TestAccountBuilder accountBuilder = anAccount().withName('Test Account'); + + // When + TestAccountBuilder clonedAccountBuilder = accountBuilder.but(); + + // Then + clonedAccountBuilder.assertEquals(accountBuilder); + + // When + Account account = accountBuilder.build(); + Account clonedAccount = clonedAccountBuilder.withName('Test Account2').build(); + + // Then + System.assertEquals('Test Account', account.Name); + System.assertEquals('Test Account2', clonedAccount.Name); + } + + /** + * @description Confirms that builder that contains a field value to another builder + * that has been built can be built + **/ + @IsTest + private static void testBuildWithRelationshipThatIsAlreadyBuilt() + { + // Build Account after estaliblishing relationship + // Given + TestAccountBuilder accountBuilder = anAccount(); + TestContactBuilder contactBuilder = aContact().withAccount(accountBuilder); + Account account = accountBuilder.build(); + + // When + Contact contact = contactBuilder.build(); + + // Then + System.assertNotEquals(null, contact.AccountId); + System.assertEquals(contact.AccountId, account.Id); + + // Before Account before establishing relationship + // Given + TestAccountBuilder accountBuilder2 = anAccount(); + Account account2 = accountBuilder2.build(); + TestContactBuilder contactBuilder2 = aContact().withAccount(accountBuilder2); + + // When + Contact contact2 = contactBuilder2.build(); + + // Then + System.assertNotEquals(null, contact2.AccountId); + System.assertEquals(contact2.AccountId, account2.Id); + } + + /** + * @description Confirms that builder that contains a field value to another builder + * that has been builtnew cannot be built + **/ + @IsTest + private static void testBuildWithRelationshipThatWasBuiltNewThrowsException() + { + runBuildWithRelationshipThatWasBuiltNew(false); + runBuildWithRelationshipThatWasBuiltNew(true); + } + + /** + * @description Confirms that builder that contains a field value to another builder + * that has been built cannot be persisted + **/ + @IsTest + private static void testPersistWithRelationshipThatWasBuiltThrowsException() + { + runPersistWithRelationshipThatWasBuilt(MethodMode.Build); + } + + /** + * @description Confirms that builder that contains a field value to another builder + * that has been builtnew cannot be persisted + **/ + @IsTest + private static void testPersistWithRelationshipThatWasBuiltNewThrowsException() + { + runPersistWithRelationshipThatWasBuilt(MethodMode.BuildNew); + } + + /** + * @description Confirms that builder that contains a field value to another builder + * that has been persisted cannot be persisted + **/ + @IsTest + private static void testPeristWithRelationshipThatWasPersistedThrowsException() + { + runPersistWithRelationshipThatWasBuilt(MethodMode.Persist); + } + + /** + * @description Confirms that builder can be persisted when one of its fields + * references another builder that has not been registered + **/ + @IsTest + private static void testPersistWithRelationshipThatIsNotRegistered() + { + // Given + TestAccountBuilder accountBuilder = anAccount().withName('Test Account'); + TestContactBuilder contactBuilder = aContact().withLastName('Test Contact').withAccount(accountBuilder).register(); + + // When + fflib_DomainObjectBuilder.persistRegistered(new fflib_SObjectUnitOfWork(UOW_SOBJECTTYPES)); + + // Then + List accounts = [SELECT Id, Name FROM Account]; + List contacts = [SELECT Id, LastName, AccountId FROM Contact]; + System.assertEquals(1, accounts.size()); + System.assertEquals(1, contacts.size()); + System.assertEquals(accounts[0].Id, contacts[0].AccountId); + System.assertEquals(2, accountBuilder.getDomainObjectBuilderEventsFired().size()); + System.assertEquals(accountBuilder.Record, accountBuilder.getDomainObjectBuilderEventsFired().get('beforeInsert')); + System.assertEquals(accountBuilder.Record, accountBuilder.getDomainObjectBuilderEventsFired().get('afterInsert')); + System.assertEquals(2, contactBuilder.getDomainObjectBuilderEventsFired().size()); + System.assertEquals(contactBuilder.Record, contactBuilder.getDomainObjectBuilderEventsFired().get('beforeInsert')); + System.assertEquals(contactBuilder.Record, contactBuilder.getDomainObjectBuilderEventsFired().get('afterInsert')); + } + + /** + * @description Confirms that multiple builders with several relationships can be persisted + **/ + @IsTest + private static void testPersistRegisteredRelationshipTree() + { + // Given + TestAccountBuilder accountBuilder1 = anAccount().withName('Test Account1'); + TestAccountBuilder accountBuilder2 = anAccount().withName('Test Account2'); + TestOpportunityBuilder opportunityBuilder1 = aClosedWonOpportunity().withAccount(accountBuilder2).register(); + TestOpportunityBuilder opportunityBuilder2 = opportunityBuilder1.but().withName('Built with but').withAccount(accountBuilder1).register(); + TestContactBuilder contactBuilder1 = aContact().withLastName('Jones').withAccount(accountBuilder1).register(); + TestContactBuilder contactBuilder2 = contactBuilder1.but().withLastName('Smith').register(); + + // When + fflib_DomainObjectBuilder.persistRegistered(new fflib_SObjectUnitOfWork(UOW_SOBJECTTYPES)); + + // Then + Map accounts = new Map([SELECT Id, Name FROM Account]); + System.assertEquals(2, accounts.size()); + Map contacts = new Map([SELECT Id, LastName, AccountId FROM Contact]); + System.assertEquals(2, contacts.size()); + Map opportunities = new Map([SELECT Id, Name, StageName, AccountId FROM Opportunity]); + System.assertEquals(2, opportunities.size()); + Account account1 = (Account)assertAndGet(accounts, accountBuilder1.Record.Id); + Account account2 = (Account)assertAndGet(accounts, opportunityBuilder1.Record.AccountId); + Contact contact1 = (Contact)assertAndGet(contacts, contactBuilder1.Record.Id); + Contact contact2 = (Contact)assertAndGet(contacts, contactBuilder2.Record.Id); + Opportunity opportunity1 = (Opportunity)assertAndGet(opportunities, opportunityBuilder1.Record.Id); + Opportunity opportunity2 = (Opportunity)assertAndGet(opportunities, opportunityBuilder2.Record.Id); + verifyRelationshipTree(account1, account2, contact1, contact2, opportunity1, opportunity2, false); + } + + /** + * @description Confirms that multiple builders with several relationships can be built + **/ + @IsTest + private static void testBuildRelationshipTree() + { + runBuildRelationshipTree(false); + runBuildRelationshipTree(true); + } + + /** + * @description Confirms that multiple builders with several layers of relationships can be built + **/ + @IsTest + private static void testBuildDeepRelationshipTree() + { + runDeepRelationshipTree(MethodMode.Build); + runDeepRelationshipTree(MethodMode.BuildNew); + } + + /** + * @description Confirms that multiple builders with several layers of relationships can be persisted + **/ + @IsTest + private static void testPersistDeepRelationshipTree() + { + runDeepRelationshipTree(MethodMode.Persist); + } + + /** + * @description Confirms that a builder instance can be persisted without being registered + **/ + @IsTest + private static void testPersistBuilder() + { + // Given + TestAccountBuilder accountBuilder = anAccount().withName('Test Account'); + + // When + Account account = accountBuilder.persist(new fflib_SObjectUnitOfWork(UOW_SOBJECTTYPES)); + + // Then + Map accounts = new Map([SELECT Id, Name FROM Account]); + System.assertEquals(true, accountBuilder.IsBuilt); + System.assertEquals(false, accountBuilder.IsRegistered); + System.assertNotEquals(null, accountBuilder.Record.Id); + System.assertEquals(account.Id, accountBuilder.Record.Id); + System.assertEquals(account, accountBuilder.Record); + System.assertEquals('Test Account', account.Name); + System.assertEquals(2, accountBuilder.getDomainObjectBuilderEventsFired().size()); + System.assertEquals(accountBuilder.Record, accountBuilder.getDomainObjectBuilderEventsFired().get('beforeInsert')); + System.assertEquals(accountBuilder.Record, accountBuilder.getDomainObjectBuilderEventsFired().get('afterInsert')); + } + + /** + * @description Confirms that a builder instance with several layers of relationships can be persisted without being registered + **/ + @IsTest + private static void testPersistRelationshipTree() + { + TestOpportunityLineItemBuilder oliBuilder = anOpportunityLineItem() + .withQuantity(5) + .withTotalPrice(500) + .withPriceBookEntry( + aPriceBookEntryWithStandardPriceBook() + .withUnitPrice(200) + .withIsActive(true) + .withProduct( + aProduct() + .withName('My Test Product'))) + .withOpportunity( + anOpportunity() + .withName('My Test Opportunity') + .withStageName('Prospecting') + .withCloseDate(System.today()) + .withAccount( + anAccount() + .withName('My Test Account'))); + + // When + OpportunityLineItem oli = oliBuilder.persist(new fflib_SObjectUnitOfWork(UOW_SOBJECTTYPES)); + + // Then + System.assertEquals(1, [SELECT COUNT() FROM Opportunity]); + System.assertEquals(1, [SELECT COUNT() FROM OpportunityLineItem]); + System.assertEquals(1, [SELECT COUNT() FROM PriceBookEntry]); + System.assertEquals(1, [SELECT COUNT() FROM Product2]); + System.assertEquals(1, [SELECT COUNT() FROM Account]); + System.assertNotEquals(null, oli.Id); + } + + /** + * @description Object Mother method for an empty account + * + * @remarks This would normally go within the TestAccountBuilder itself + * but static methods are not allowed on inner classes. + **/ + private static TestAccountBuilder anAccount() { + return new TestAccountBuilder(); + } + + /** + * @description Object Mother method for an Prospect account + * + * @remarks This would normally go within the TestAccountBuilder itself + * but static methods are not allowed on inner classes. + **/ + private static TestAccountBuilder aProspect() { + return anAccount().withName('Potential Customer').withType('Prospect'); + } + + /** + * @description Object Mother method for an Empty contact + * + * @remarks This would normally go within the TestContactBuilder itself + * but static methods are not allowed on inner classes. + **/ + private static TestContactBuilder aContact() { + return new TestContactBuilder(); + } + + /** + * @description Object Mother method for a Contact with an Account + * + * @remarks This would normally go within the TestContactBuilder itself + * but static methods are not allowed on inner classes. + **/ + private static TestContactBuilder aContactWithAccount() { + return aContact().withLastName('Test Contact').withAccount(anAccount().withName('Test Account')); + } + + /** + * @description Object Mother method for an Empty Opportunity + * + * @remarks This would normally go within the TestOpportunityBuilder itself + * but static methods are not allowed on inner classes. + **/ + private static TestOpportunityBuilder anOpportunity() { + return new TestOpportunityBuilder(); + } + + /** + * @description Object Mother method for a Closed Won Opportunity + * + * @remarks This would normally go within the TestOpportunityBuilder itself + * but static methods are not allowed on inner classes. + **/ + private static TestOpportunityBuilder aClosedWonOpportunity() { + return anOpportunity() + .withName('Large Purchase') + .withAccount(aProspect()) + .withAmount(1000000.00) + .withStageName('Closed Won') + .withType('New Customer') + .withCloseDate(System.today()); + } + + /** + * @description Object Mother method for an Empty Opportunity Line Item + * + * @remarks This would normally go within the TestOpportunityLineItemBuilder itself + * but static methods are not allowed on inner classes. + **/ + private static TestOpportunityLineItemBuilder anOpportunityLineItem() { + return new TestOpportunityLineItemBuilder(); + } + + /** + * @description Object Mother method for an Empty Price Book + * + * @remarks This would normally go within the TestPriceBookBuilder itself + * but static methods are not allowed on inner classes. + **/ + private static TestPriceBookBuilder aPriceBook() { + return new TestPriceBookBuilder(); + } + + /** + * @description Object Mother method for an Empty Price Book Entry + * + * @remarks This would normally go within the TestPriceBookEntryBuilder itself + * but static methods are not allowed on inner classes. + **/ + private static TestPriceBookEntryBuilder aPriceBookEntry() { + return new TestPriceBookEntryBuilder(); + } + + /** + * @description Object Mother method for Price Book Entry that uses the Standard Price Book + * + * @remarks This would normally go within the TestPriceBookEntryBuilder itself + * but static methods are not allowed on inner classes. + **/ + private static TestPriceBookEntryBuilder aPriceBookEntryWithStandardPriceBook() { + return aPriceBookEntry().withStandardPriceBook(); + } + + /** + * @description Object Mother method for an Empty Product + * + * @remarks This would normally go within the TestProductBuilder itself + * but static methods are not allowed on inner classes. + **/ + private static TestProductBuilder aProduct() { + return new TestProductBuilder(); + } + + private abstract class TestBuilderBase extends fflib_DomainObjectBuilder + { + /** + * @description Tracks events fired on this instance to aid in test verifications + **/ + private Map m_domainObjectBuilderEvents = new Map(); + + protected TestBuilderBase(SObjectType type) { + super(type); + } + + protected TestBuilderBase(TestBuilderBase copyFrom) { + super(copyFrom); + } + + public Map getDomainObjectBuilderEventsFired() { + return m_domainObjectBuilderEvents.clone(); + } + + protected virtual override void beforeBuild(Boolean isNew) { + super.beforeBuild(isNew); + addEvent('beforeBuild', isNew); + } + + protected virtual override void afterBuild(SObject record) { + super.afterBuild(record); + addEvent('afterBuild', record); + } + + protected virtual override void beforeInsert(SObject record) { + super.beforeInsert(record); + addEvent('beforeInsert', record); + } + + protected virtual override void afterInsert(SObject record) { + super.afterInsert(record); + addEvent('afterInsert', record); + } + + protected void addEvent(String eventName, Object value) { + if (m_domainObjectBuilderEvents.containsKey(eventName)) { + throw new TestDomainBuilderException(String.format('Event {0} has already been fired.', new List { eventName })); + } + + m_domainObjectBuilderEvents.put(eventName, value); + } + + /** + * @description Helper to provide test support to gain access to protected members + **/ + protected void assertEquals(TestAccountBuilder compareTo) { + System.assertEquals(this.m_fieldValueMap, compareTo.m_fieldValueMap); + System.assertEquals(this.m_parentByRelationship, compareTo.m_parentByRelationship); + System.assertEquals(this.m_sObjType, compareTo.m_sObjType); + } + } + + private class TestAccountBuilder extends TestBuilderBase + { + /** + * Methods/Properties below would be included in a basic template for any derived builder class + * + * BEGIN STANDARD BUILDER TEMPLATE + * ------------------------------- + **/ + + /** + * @description Constructor should be included in every derived builder + **/ + private TestAccountBuilder() { + super(Account.SObjectType); + } + + /** + * @description Constructor should be included in every derived builder + **/ + private TestAccountBuilder(TestAccountBuilder copyFrom) { + super(copyFrom); + } + + /** + * @description Creates an existing SObject without issuing DML + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public Account build() { + return (Account)build(false); + } + + /** + * @description Creates an New SObject (No Id) without issuing DML + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public Account buildNew() { + return (Account)build(true); + } + + /** + * @description Persists builder and its related data through Unit Of Work + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public Account persist(fflib_ISObjectUnitOfWork uow) { + return (Account)persistBuilder(uow); + } + + /** + * @description Registers instance for persistance via persistBuilders + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public TestAccountBuilder register() { + return (TestAccountBuilder)registerBuilder(); + } + + /** + * @description Returns Contact SObject associated to this builder + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public Account Record { + get { return (Account)getRecord(); } + private set; + } + + /** + * @description Returns a Clone of this instance + **/ + public TestAccountBuilder but() { + return new TestAccountBuilder(this); + } + + /** + * Methods/Properties above would be included in a basic template for any derived builder class + * + * END STANDARD BUILDER TEMPLATE + * ------------------------------- + **/ + + /** + * @description Remaining methods are SObject specific and support fluent configuration of field values + **/ + public TestAccountBuilder withName(String value) { + set(Account.Name, value); + return this; + } + + public TestAccountBuilder withType(String value) { + set(Account.Type, value); + return this; + } + } + + private class TestContactBuilder extends TestBuilderBase + { + /** + * Methods/Properties below would be included in a basic template for any derived builder class + * + * BEGIN STANDARD BUILDER TEMPLATE + * ------------------------------- + **/ + + /** + * @description Constructor should be included in every derived builder + **/ + private TestContactBuilder() { + super(Contact.SObjectType); + } + + /** + * @description Constructor should be included in every derived builder + **/ + private TestContactBuilder(TestContactBuilder copyFrom) { + super(copyFrom); + } + + /** + * @description Creates an existing SObject without issuing DML + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public Contact build() { + return (Contact)build(false); + } + + /** + * @description Creates an New SObject (No Id) without issuing DML + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public Contact buildNew() { + return (Contact)build(true); + } + + /** + * @description Persists builder and its related data through Unit Of Work + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public Contact persist(fflib_ISObjectUnitOfWork uow) { + return (Contact)persistBuilder(uow); + } + + /** + * @description Registers instance for persistance via persistBuilders + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public TestContactBuilder register() { + return (TestContactBuilder)registerBuilder(); + } + + /** + * @description Returns Contact SObject associated to this builder + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public Contact Record { + get { return (Contact)getRecord(); } + private set; + } + + /** + * @description Returns a Clone of this instance + **/ + public TestContactBuilder but() { + return new TestContactBuilder(this); + } + + /** + * Methods/Properties above would be included in a basic template for any derived builder class + * + * END STANDARD BUILDER TEMPLATE + * ------------------------------- + **/ + + /** + * @description Remaining methods are SObject specific and support fluent configuration of field values + **/ + public TestContactBuilder withLastName(String value) { + set(Contact.LastName, value); + return this; + } + + public TestContactBuilder withFirstName(String value) { + set(Contact.FirstName, value); + return this; + } + + public TestContactBuilder withAccount(TestAccountBuilder value) { + setParent(Contact.AccountId, value); + return this; + } + } + + private class TestOpportunityBuilder extends TestBuilderBase + { + /** + * Methods/Properties below would be included in a basic template for any derived builder class + * + * BEGIN STANDARD BUILDER TEMPLATE + * ------------------------------- + **/ + + /** + * @description Constructor should be included in every derived builder + **/ + private TestOpportunityBuilder() { + super(Opportunity.SObjectType); + } + + /** + * @description Constructor should be included in every derived builder + **/ + private TestOpportunityBuilder(TestOpportunityBuilder copyFrom) { + super(copyFrom); + } + + /** + * @description Creates an existing SObject without issuing DML + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public Opportunity build() { + return (Opportunity)build(false); + } + + /** + * @description Creates an New SObject (No Id) without issuing DML + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public Opportunity buildNew() { + return (Opportunity)build(true); + } + + /** + * @description Persists builder and its related data through Unit Of Work + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public Opportunity persist(fflib_ISObjectUnitOfWork uow) { + return (Opportunity)persistBuilder(uow); + } + + /** + * @description Registers instance for persistance via persistBuilders + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public TestOpportunityBuilder register() { + return (TestOpportunityBuilder)registerBuilder(); + } + + /** + * @description Returns Contact SObject associated to this builder + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public Opportunity Record { + get { return (Opportunity)getRecord(); } + private set; + } + + /** + * @description Returns a Clone of this instance + **/ + public TestOpportunityBuilder but() { + return new TestOpportunityBuilder(this); + } + + /** + * Methods/Properties above would be included in a basic template for any derived builder class + * + * END STANDARD BUILDER TEMPLATE + * ------------------------------- + **/ + + /** + * @description Remaining methods are SObject specific and support fluent configuration of field values + **/ + public TestOpportunityBuilder withAccount(TestAccountBuilder value) { + setParent(Opportunity.AccountId, value); + return this; + } + + public TestOpportunityBuilder withName(String value) { + set(Opportunity.Name, value); + return this; + } + + public TestOpportunityBuilder withAmount(Decimal value) { + set(Opportunity.Amount, value); + return this; + } + + public TestOpportunityBuilder withStageName(String value) { + set(Opportunity.StageName, value); + return this; + } + + public TestOpportunityBuilder withCloseDate(Date value) { + set(Opportunity.CloseDate, value); + return this; + } + + public TestOpportunityBuilder withType(String value) { + set(Opportunity.Type, value); + return this; + } + } + + private class TestOpportunityLineItemBuilder extends TestBuilderBase + { + /** + * Methods/Properties below would be included in a basic template for any derived builder class + * + * BEGIN STANDARD BUILDER TEMPLATE + * ------------------------------- + **/ + + /** + * @description Constructor should be included in every derived builder + **/ + private TestOpportunityLineItemBuilder() { + super(OpportunityLineItem.SObjectType); + } + + /** + * @description Constructor should be included in every derived builder + **/ + private TestOpportunityLineItemBuilder(TestOpportunityLineItemBuilder copyFrom) { + super(copyFrom); + } + + /** + * @description Creates an existing SObject without issuing DML + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public OpportunityLineItem build() { + return (OpportunityLineItem)build(false); + } + + /** + * @description Creates an New SObject (No Id) without issuing DML + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public OpportunityLineItem buildNew() { + return (OpportunityLineItem)build(true); + } + + /** + * @description Persists builder and its related data through Unit Of Work + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public OpportunityLineItem persist(fflib_ISObjectUnitOfWork uow) { + return (OpportunityLineItem)persistBuilder(uow); + } + + /** + * @description Registers instance for persistance via persistBuilders + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public TestOpportunityLineItemBuilder register() { + return (TestOpportunityLineItemBuilder)registerBuilder(); + } + + /** + * @description Returns Contact SObject associated to this builder + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public OpportunityLineItem Record { + get { return (OpportunityLineItem)getRecord(); } + private set; + } + + /** + * @description Returns a Clone of this instance + **/ + public TestOpportunityLineItemBuilder but() { + return new TestOpportunityLineItemBuilder(this); + } + + /** + * Methods/Properties above would be included in a basic template for any derived builder class + * + * END STANDARD BUILDER TEMPLATE + * ------------------------------- + **/ + + /** + * @description Remaining methods are SObject specific and support fluent configuration of field values + **/ + public TestOpportunityLineItemBuilder withQuantity(Decimal value) { + set(OpportunityLineItem.Quantity, value); + return this; + } + + public TestOpportunityLineItemBuilder withTotalPrice(Decimal value) { + set(OpportunityLineItem.TotalPrice, value); + return this; + } + + public TestOpportunityLineItemBuilder withPriceBookEntry(TestPriceBookEntryBuilder value) { + setParent(OpportunityLineItem.PriceBookEntryId, value); + return this; + } + + public TestOpportunityLineItemBuilder withOpportunity(TestOpportunityBuilder value) { + setParent(OpportunityLineItem.OpportunityId, value); + return this; + } + } + + private class TestProductBuilder extends TestBuilderBase + { + /** + * Methods/Properties below would be included in a basic template for any derived builder class + * + * BEGIN STANDARD BUILDER TEMPLATE + * ------------------------------- + **/ + + /** + * @description Constructor should be included in every derived builder + **/ + private TestProductBuilder() { + super(Product2.SObjectType); + } + + /** + * @description Constructor should be included in every derived builder + **/ + private TestProductBuilder(TestProductBuilder copyFrom) { + super(copyFrom); + } + + /** + * @description Creates an existing SObject without issuing DML + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public Product2 build() { + return (Product2)build(false); + } + + /** + * @description Creates an New SObject (No Id) without issuing DML + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public Product2 buildNew() { + return (Product2)build(true); + } + + /** + * @description Persists builder and its related data through Unit Of Work + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public Product2 persist(fflib_ISObjectUnitOfWork uow) { + return (Product2)persistBuilder(uow); + } + + /** + * @description Registers instance for persistance via persistBuilders + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public TestProductBuilder register() { + return (TestProductBuilder)registerBuilder(); + } + + /** + * @description Returns Contact SObject associated to this builder + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public Product2 Record { + get { return (Product2)getRecord(); } + private set; + } + + /** + * @description Returns a Clone of this instance + **/ + public TestProductBuilder but() { + return new TestProductBuilder(this); + } + + /** + * Methods/Properties above would be included in a basic template for any derived builder class + * + * END STANDARD BUILDER TEMPLATE + * ------------------------------- + **/ + + /** + * @description Remaining methods are SObject specific and support fluent configuration of field values + **/ + public TestProductBuilder withName(String value) { + set(Product2.Name, value); + return this; + } + } + + private class TestPriceBookBuilder extends TestBuilderBase + { + /** + * Methods/Properties below would be included in a basic template for any derived builder class + * + * BEGIN STANDARD BUILDER TEMPLATE + * ------------------------------- + **/ + + /** + * @description Constructor should be included in every derived builder + **/ + private TestPriceBookBuilder() { + super(Pricebook2.SObjectType); + } + + /** + * @description Constructor should be included in every derived builder + **/ + private TestPriceBookBuilder(TestPriceBookBuilder copyFrom) { + super(copyFrom); + } + + /** + * @description Creates an existing SObject without issuing DML + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public Pricebook2 build() { + return (Pricebook2)build(false); + } + + /** + * @description Creates an New SObject (No Id) without issuing DML + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public Pricebook2 buildNew() { + return (Pricebook2)build(true); + } + + /** + * @description Persists builder and its related data through Unit Of Work + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public Pricebook2 persist(fflib_ISObjectUnitOfWork uow) { + return (Pricebook2)persistBuilder(uow); + } + + /** + * @description Registers instance for persistance via persistBuilders + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public TestPriceBookBuilder register() { + return (TestPriceBookBuilder)registerBuilder(); + } + + /** + * @description Returns Contact SObject associated to this builder + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public Pricebook2 Record { + get { return (Pricebook2)getRecord(); } + private set; + } + + /** + * @description Returns a Clone of this instance + **/ + public TestPriceBookBuilder but() { + return new TestPriceBookBuilder(this); + } + + /** + * Methods/Properties above would be included in a basic template for any derived builder class + * + * END STANDARD BUILDER TEMPLATE + * ------------------------------- + **/ + + /** + * @description Remaining methods are SObject specific and support fluent configuration of field values + **/ + public TestPriceBookBuilder withName(String value) { + set(Pricebook2.Name, value); + return this; + } + + public TestPriceBookBuilder withIsActive(Boolean value) { + set(Pricebook2.IsActive, value); + return this; + } + } + + private class TestPriceBookEntryBuilder extends TestBuilderBase + { + /** + * Methods/Properties below would be included in a basic template for any derived builder class + * + * BEGIN STANDARD BUILDER TEMPLATE + * ------------------------------- + **/ + + /** + * @description Constructor should be included in every derived builder + **/ + private TestPriceBookEntryBuilder() { + super(PricebookEntry.SObjectType); + } + + /** + * @description Constructor should be included in every derived builder + **/ + private TestPriceBookEntryBuilder(TestPriceBookEntryBuilder copyFrom) { + super(copyFrom); + } + + /** + * @description Creates an existing SObject without issuing DML + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public PricebookEntry build() { + return (PricebookEntry)build(false); + } + + /** + * @description Creates an New SObject (No Id) without issuing DML + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public PricebookEntry buildNew() { + return (PricebookEntry)build(true); + } + + /** + * @description Persists builder and its related data through Unit Of Work + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public PricebookEntry persist(fflib_ISObjectUnitOfWork uow) { + return (PricebookEntry)persistBuilder(uow); + } + + /** + * @description Registers instance for persistance via persistBuilders + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public TestPriceBookEntryBuilder register() { + return (TestPriceBookEntryBuilder)registerBuilder(); + } + + /** + * @description Returns Contact SObject associated to this builder + * + * @remarks Wrapper method to base class to allow for casting of specific SObjectType + **/ + public PricebookEntry Record { + get { return (PricebookEntry)getRecord(); } + private set; + } + + /** + * @description Returns a Clone of this instance + **/ + public TestPriceBookEntryBuilder but() { + return new TestPriceBookEntryBuilder(this); + } + + /** + * Methods/Properties above would be included in a basic template for any derived builder class + * + * END STANDARD BUILDER TEMPLATE + * ------------------------------- + **/ + + /** + * @description Remaining methods are SObject specific and support fluent configuration of field values + **/ + public TestPriceBookEntryBuilder withUnitPrice(Decimal value) { + set(PricebookEntry.UnitPrice, value); + return this; + } + + public TestPriceBookEntryBuilder withIsActive(Boolean value) { + set(PricebookEntry.IsActive, value); + return this; + } + + public TestPriceBookEntryBuilder withUseStandardPrice(Boolean value) { + set(PriceBookEntry.UseStandardPrice, value); + return this; + } + + public TestPriceBookEntryBuilder withStandardPriceBook() { + set(PriceBookEntry.PriceBook2Id, Test.getStandardPricebookId()); + return this; + } + + public TestPriceBookEntryBuilder withPriceBook(TestPriceBookBuilder value) { + setParent(PriceBookEntry.PriceBook2Id, value); + return this; + } + + public TestPriceBookEntryBuilder withProduct(TestProductBuilder value) { + setParent(PriceBookEntry.Product2Id, value); + return this; + } + } + + /** + * General exception class for test class + **/ + public class TestDomainBuilderException extends Exception {} +} \ No newline at end of file diff --git a/fflib/src/classes/fflib_DomainObjectBuilderTest.cls-meta.xml b/fflib/src/classes/fflib_DomainObjectBuilderTest.cls-meta.xml new file mode 100644 index 00000000000..b12420ea0e7 --- /dev/null +++ b/fflib/src/classes/fflib_DomainObjectBuilderTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 31.0 + Active + diff --git a/fflib/src/package.xml b/fflib/src/package.xml index b7e70f68e1f..406fb64b121 100644 --- a/fflib/src/package.xml +++ b/fflib/src/package.xml @@ -3,6 +3,8 @@ fflib_Application fflib_ApplicationTest + fflib_DomainObjectBuilder + fflib_DomainObjectBuilderTest fflib_ISObjectDomain fflib_ISObjectSelector fflib_ISObjectUnitOfWork