├── .gitignore ├── LICENSE ├── README.md ├── config └── project-scratch-def.json ├── sfdx-project.json └── sfdx-source └── apex-common-builder ├── main └── classes │ ├── fflib_DomainObjectBuilder.cls │ └── fflib_DomainObjectBuilder.cls-meta.xml └── test └── classes ├── fflib_DomainObjectBuilderTest.cls └── fflib_DomainObjectBuilderTest.cls-meta.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .sfdx/ 3 | .sf/ 4 | .vscode 5 | .idea 6 | IlluminatedCloud 7 | /temp 8 | /target 9 | package-lock.json 10 | package.json 11 | /node_modules 12 | .history/ 13 | /research 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, FinancialForce for Developers 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fflib-apex-common-builder 2 | Builder Extension library to fflib-apex-common. 3 | 4 | This is currently work in progress. 5 | 6 | Setup 7 | ----- 8 | This [wiki article](https://github.com/apex-enterprise-patterns/fflib-apex-common-builder/wiki/Environment-Setup) gives instructions on how to setup the codebase in a SalesforceDX Scratch org. 9 | 10 | Background and Community Discussion 11 | ----------------------------------- 12 | See the discussion in the [fflib-apex-common PR #77](https://github.com/apex-enterprise-patterns/fflib-apex-common/pull/77) and [fflib-apex-common PR #100](https://github.com/apex-enterprise-patterns/fflib-apex-common/pull/100). 13 | 14 | 15 | 16 | Sample Usage 17 | ------------ 18 | See the conversation in the [fflib-apex-common-samplecode PR #6](https://github.com/apex-enterprise-patterns/fflib-apex-common-samplecode/pull/6). -------------------------------------------------------------------------------- /config/project-scratch-def.json: -------------------------------------------------------------------------------- 1 | { 2 | "orgName": "apex-common-builder", 3 | "edition": "Developer", 4 | "settings": { 5 | "lightningExperienceSettings": { 6 | "enableS1DesktopEnabled": true 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /sfdx-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageDirectories": [ 3 | { 4 | "path": "sfdx-source/apex-common-builder", 5 | "default": true 6 | } 7 | ], 8 | "namespace": "", 9 | "sfdcLoginUrl": "https://login.salesforce.com", 10 | "sourceApiVersion": "52.0" 11 | } -------------------------------------------------------------------------------- /sfdx-source/apex-common-builder/main/classes/fflib_DomainObjectBuilder.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * BSD 3-Clause License 3 | * 4 | * Copyright (c) 2017, FinancialForce for Developers 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * - Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 13 | * - Redistributions in binary form must reproduce the above copyright notice, 14 | * this list of conditions and the following disclaimer in the documentation 15 | * and/or other materials provided with the distribution. 16 | * 17 | * - Neither the name of the copyright holder nor the names of its 18 | * contributors may be used to endorse or promote products derived from 19 | * this software without specific prior written permission. 20 | * 21 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | **/ 32 | 33 | /** 34 | * Base class aiding in the implementation of Test Data Builder pattern as described by Nate Pryce 35 | * (http://www.natpryce.com/). Derived classes can combine the features the Builder 36 | * Pattern (http://www.c2.com/cgi/wiki?BuilderPattern) with an Object Mother pattern 37 | * (http://www.c2.com/cgi/wiki?ObjectMother) to simplify the setup/creation of data. 38 | * 39 | * While test data in unit tests should be as simple as possible and could even be mocked with frameworks like ApexMock, 40 | * complex enterprise software also needs integration tests. The TestDataBuilder pattern reduces complexity, 41 | * eliminates redundancy and improves readability while setting up complex test data structures for both 42 | * unit and integration tests and can also be used for production purposes. 43 | * 44 | * Learn how to replace your redundant, error-prone and test-specific helper classes with a set of small domain classes 45 | * and how to write more readable tests by checking the samples in the test class fflib_DomainObjectBuilderTest 46 | */ 47 | public abstract class fflib_DomainObjectBuilder 48 | { 49 | /** 50 | * @description Maintains non-relationship field values that have been assigned to an instance of this class 51 | **/ 52 | protected Map m_fieldValueMap = new Map(); 53 | 54 | /** 55 | * @description Maintains relationship field values that have been assigned to an instance of this class 56 | * Relationship field values are references to builder instances 57 | **/ 58 | protected Map m_parentByRelationship = new Map(); 59 | 60 | /** 61 | * @description The SObjectType this Builder class represents 62 | **/ 63 | protected Schema.SObjectType m_sObjType; 64 | 65 | /** 66 | * @description The SObject this Builder class is building 67 | **/ 68 | protected SObject m_sObjRecord { get; protected set; } 69 | 70 | /** 71 | * @description Tracks all builders that have registered to be persisted to a UnitOfWork 72 | **/ 73 | private static Set m_registeredBuilders = new Set(); 74 | 75 | /** 76 | * @description Constructs the Builder class with the specified SObjectType 77 | * 78 | * @param type The SObject type that the builder will build 79 | **/ 80 | protected fflib_DomainObjectBuilder(SObjectType type) { 81 | this.m_sObjType = type; 82 | // passing false because of bug in SFDC - See https://success.salesforce.com/issues_view?id=a1p30000000Sz5RAAS 83 | // not establishing defaults also gives us an opportunity for a completely "clean" record 84 | this.m_sObjRecord = m_sObjType.newSObject(null, false); 85 | this.IsBuilt = false; 86 | this.IsRegistered = false; 87 | } 88 | 89 | /** 90 | * @description Copy Constructor that constructs the Builder class based on the builder specified 91 | * 92 | * @param copyFrom The builder to copy/clone this instance from 93 | **/ 94 | protected fflib_DomainObjectBuilder(fflib_DomainObjectBuilder copyFrom) 95 | { 96 | this(copyFrom.getSObjectType()); 97 | m_fieldValueMap = copyFrom.m_fieldValueMap.clone(); 98 | m_parentByRelationship = copyFrom.m_parentByRelationship.clone(); 99 | } 100 | 101 | /** 102 | * @description Commits all registered builders and their related builders to the Unit Of Work specified 103 | * 104 | * @param uow The Unit Of Work to process against 105 | **/ 106 | public static void persistRegistered(fflib_ISObjectUnitOfWork uow) { 107 | persist(uow, m_registeredBuilders); 108 | } 109 | 110 | /** 111 | * @description Returns true if builder has been built, false otherwise 112 | * A builder IsBuilt build or persist operations have completed successfully on it 113 | **/ 114 | public Boolean IsBuilt { get; protected set; } 115 | 116 | /** 117 | * @description Returns true if builder has been registered, false otherwise 118 | * A builder IsRegistered if registerBuilder has been called on it and the 119 | * builder has not yet been built/persisted 120 | **/ 121 | public Boolean IsRegistered { get; protected set; } 122 | 123 | /** 124 | * @description Returns the SObject associated to this builder 125 | **/ 126 | public SObject getRecord() { 127 | return m_sObjRecord; 128 | } 129 | 130 | /** 131 | * @description Returns the SObjectType associated to this builder 132 | **/ 133 | public virtual Schema.SObjectType getSObjectType() { 134 | return m_sObjType; 135 | } 136 | 137 | /** 138 | * @description Returns the SObject after building the builder and any of its related builders 139 | * 140 | * @param isNew True if the instance should not have an Id value (new record), false otherwise (existing record) 141 | * 142 | * @remarks Recommended to wrap this method with a 'build()' and 'buildNew()' in a derived class 143 | * casting to the derived type to support fluent configuration 144 | **/ 145 | public virtual SObject build(Boolean isNew) 146 | { 147 | // see if we'll let 'em through 148 | checkAllowBuild(); 149 | 150 | // fire event 151 | beforeBuild(isNew); 152 | 153 | // set non-relationship field values 154 | for (Schema.SObjectField field : this.m_fieldValueMap.keySet()) { 155 | m_sObjRecord.put(field, this.m_fieldValueMap.get(field)); 156 | } 157 | 158 | // set relationship field values 159 | for(Schema.SObjectField rel: this.m_parentByRelationship.keySet()) { 160 | // get the related builder 161 | fflib_DomainObjectBuilder parent = this.m_parentByRelationship.get(rel); 162 | 163 | // if not built yet, we must, we must 164 | if (!parent.IsBuilt) { 165 | // should not be building a builder that is registered 166 | if (parent.IsRegistered) { 167 | throw new DomainObjectBuilderException(String.format('Field {0} contains value for a builder that is marked for registration', new List { rel.getDescribe().getName() })); 168 | } 169 | 170 | // related object should always be trated as existing 171 | parent.build(false); 172 | // we must have an Id value for all relationship fields - if Id is Null builder 173 | // was built using isNew = true or Id has been cleared manually 174 | } else if (null == parent.m_sObjRecord.Id) { 175 | throw new DomainObjectBuilderException(String.format('Field {0} contains value for a builder that was built as new', new List { rel.getDescribe().getName() })); 176 | } 177 | 178 | // set field value with related SObject Id 179 | m_sObjRecord.put(rel, parent.m_sObjRecord.Id); 180 | } 181 | 182 | // establish Id value 183 | m_sObjRecord.Id = isNew ? null : fflib_IDGenerator.generate(getSObjectType()); 184 | 185 | // update state of builder 186 | markAsBuilt(); 187 | 188 | // fire event 189 | afterBuild(m_sObjRecord); 190 | 191 | // let 'em have it 192 | return m_sObjRecord; 193 | } 194 | 195 | /** 196 | * @description Commits this builder and its related builders to the Unit Of Work specified 197 | * 198 | * @param uow The Unit Of Work to process against 199 | * 200 | * @remarks Recommended to wrap this method with a 'persist' method in a derived class 201 | * casting to the derived type to support fluent configuration 202 | **/ 203 | public virtual SObject persistBuilder(fflib_ISObjectUnitOfWork uow) 204 | { 205 | // persist this builder 206 | persist(uow, new Set { this }); 207 | 208 | // let 'em have it 209 | return m_sObjRecord; 210 | } 211 | 212 | /** 213 | * @description Registers builder to be processed by persistRegistered 214 | * 215 | * Returns itself to support fluent configuration 216 | * 217 | * @remarks Recommended to wrap this method with a 'register()' method in a derived class 218 | * casting to the derived type to support fluent configuration 219 | **/ 220 | protected virtual fflib_DomainObjectBuilder registerBuilder() { 221 | // see if we'll let 'em through 222 | checkAllowRegister(); 223 | 224 | // add to registered list 225 | m_registeredBuilders.add(this); 226 | 227 | // mark as registered 228 | markAsRegistered(); 229 | 230 | // return the builder itself 231 | return this; 232 | } 233 | 234 | /** 235 | * @description Validates that builder is not registered 236 | * 237 | * @throws DomainObjectBuilderException if builder is already registered 238 | **/ 239 | protected virtual void checkIsRegistered() { 240 | if (IsRegistered) { 241 | throw new DomainObjectBuilderException('Builder has already been registered'); 242 | } 243 | } 244 | 245 | /** 246 | * @description Validates that builder is not built 247 | * 248 | * @throws DomainObjectBuilderException if builder is already built 249 | **/ 250 | protected virtual void checkIsBuilt() { 251 | if (IsBuilt) { 252 | throw new DomainObjectBuilderException('Builder has already been built'); 253 | } 254 | } 255 | 256 | /** 257 | * @description Validates that builder can be built 258 | * 259 | * @throws DomainObjectBuilderException if builder is registered or is already built 260 | **/ 261 | protected virtual void checkAllowBuild() 262 | { 263 | // should not allow build if builder is registered 264 | checkIsRegistered(); 265 | 266 | // should not allow build if already built 267 | checkIsBuilt(); 268 | } 269 | 270 | /** 271 | * @description Validates that builder can be registered 272 | * 273 | * @throws DomainObjectBuilderException if builder is registered or is already built 274 | **/ 275 | protected virtual void checkAllowRegister() 276 | { 277 | // should not register a builder that has already been registered 278 | checkIsRegistered(); 279 | 280 | // should not register a builder that has been built 281 | checkIsBuilt(); 282 | } 283 | 284 | /** 285 | * @description Validates that builder can be changed 286 | * 287 | * @throws DomainObjectBuilderException if builder is built 288 | **/ 289 | protected virtual void checkAllowSet() 290 | { 291 | // should not change data on builder after its built 292 | checkIsBuilt(); 293 | } 294 | 295 | /** 296 | * @description This method allows subclasses to invoke any action before the SObject is built. 297 | * 298 | * @param isNew True if building as new, false otherwise (existing) 299 | **/ 300 | protected virtual void beforeBuild(Boolean isNew) {} 301 | 302 | /** 303 | * @description This method allows subclasses to handle the SObject after it is built. 304 | * 305 | * @param sObj The SObject that has been built. 306 | */ 307 | protected virtual void afterBuild(SObject sObj) {} 308 | 309 | /** 310 | * @description This method allows subclasses to handle the SObject before it is inserted 311 | * 312 | * @param sObj The SObject that has been built. 313 | */ 314 | protected virtual void beforeInsert(SObject sObj) {} 315 | 316 | /** 317 | * @description This method allows subclasses to handle the SObject after it is inserted. 318 | * 319 | * @param sObj The SObject that has been built. 320 | */ 321 | protected virtual void afterInsert(SObject sObj) {} 322 | 323 | /** 324 | * @description Updates the builder state to indicate it has been built 325 | */ 326 | protected virtual void markAsBuilt() 327 | { 328 | // mark as built 329 | IsBuilt = true; 330 | // clear registered flag (might or might not have been registered depending on which 'build/persist' process was used 331 | IsRegistered = false; 332 | } 333 | 334 | /** 335 | * @description Updates the builder state to indicate it has been registered 336 | */ 337 | protected virtual void markAsRegistered() 338 | { 339 | // mark as registered 340 | IsRegistered = true; 341 | } 342 | 343 | /** 344 | * @description Update the builder field with the value specified 345 | * 346 | * @param field The SObjectField to set the value on 347 | * @param value The value for the SObjectField 348 | */ 349 | protected virtual fflib_DomainObjectBuilder set(Schema.SObjectField field, Object value) { 350 | // should not allow changing data after built 351 | checkAllowSet(); 352 | 353 | // set field value 354 | m_fieldValueMap.put(field, value); 355 | 356 | // right back at ya 357 | return this; 358 | } 359 | 360 | /** 361 | * @description Updates the builder field with the value specified 362 | * 363 | * @param parentRelationship The SObjectField to set the value on 364 | * @param parent The builder instance for the SObjectField 365 | */ 366 | protected virtual fflib_DomainObjectBuilder setParent(Schema.SObjectField parentRelationship, fflib_DomainObjectBuilder parent) { 367 | // should not allow changing data after built 368 | checkAllowSet(); 369 | 370 | // Note: The parent registered last always wins! 371 | m_parentByRelationship.put(parentRelationship, parent); 372 | 373 | // give and you shall receive 374 | return this; 375 | } 376 | 377 | /** 378 | * @description Commits the set of builders specified and their related builders to the Unit Of Work specified 379 | * 380 | * @param uow The Unit Of Work to process against 381 | * @param builders The builders to persist to the Unit Of Work 382 | **/ 383 | private static void persist(fflib_ISObjectUnitOfWork uow, Set builders) { 384 | // track what we've prepared 385 | Set preparedBuilders = new Set(); 386 | 387 | // iterate builders processing each one 388 | for (fflib_DomainObjectBuilder builder :builders) { 389 | prepareForCommit(uow, builder, preparedBuilders); 390 | } 391 | 392 | // commit work 393 | uow.commitWork(); 394 | 395 | // iterate all builders marking them built 396 | for (fflib_DomainObjectBuilder builder :preparedBuilders) { 397 | // mark as built 398 | builder.markAsBuilt(); 399 | 400 | // fire event 401 | builder.afterInsert(builder.m_sObjRecord); 402 | } 403 | 404 | // reset builders Set 405 | builders.clear(); 406 | } 407 | 408 | /** 409 | * @description Prepares a builder to be committed through a Unit Of Work 410 | * 411 | * @param uow The Unit Of Work to process against 412 | * @param builder The builder to prepare 413 | * @param preparedBuilders The builders that have already been prepared during the current operation 414 | **/ 415 | private static Set prepareForCommit(fflib_ISObjectUnitOfWork uow, fflib_DomainObjectBuilder builder, Set preparedBuilders) { 416 | // fire event 417 | builder.beforeInsert(builder.m_sObjRecord); 418 | 419 | // set non-relationship field values 420 | for (Schema.SObjectField field : builder.m_fieldValueMap.keySet()) { 421 | builder.m_sObjRecord.put(field, builder.m_fieldValueMap.get(field)); 422 | } 423 | 424 | // set relationship field values 425 | for(Schema.SObjectField rel: builder.m_parentByRelationship.keySet()) { 426 | // get related builder 427 | fflib_DomainObjectBuilder parent = builder.m_parentByRelationship.get(rel); 428 | 429 | // if its not in the registered list, we must manually add it to be persisted 430 | if (!parent.IsRegistered) { 431 | // cannot persist a builder that has been built 432 | if (parent.IsBuilt) { 433 | throw new DomainObjectBuilderException(String.format('Field {0} contains value for a builder that has already been built', new List { rel.getDescribe().getName() })); 434 | } 435 | if (!preparedBuilders.contains(parent)) { 436 | prepareForCommit(uow, parent, preparedBuilders); 437 | } 438 | } 439 | 440 | uow.registerRelationship(builder.m_sObjRecord, rel, parent.m_sObjRecord); 441 | } 442 | 443 | // register as new 444 | uow.registerNew(builder.m_sObjRecord); 445 | 446 | // we've completed the preparation 447 | preparedBuilders.add(builder); 448 | 449 | return preparedBuilders; 450 | } 451 | 452 | /** 453 | * General exception class for builders 454 | **/ 455 | public class DomainObjectBuilderException extends Exception {} 456 | } -------------------------------------------------------------------------------- /sfdx-source/apex-common-builder/main/classes/fflib_DomainObjectBuilder.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 63.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /sfdx-source/apex-common-builder/test/classes/fflib_DomainObjectBuilderTest.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * BSD 3-Clause License 3 | * 4 | * Copyright (c) 2017, FinancialForce for Developers 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * - Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 13 | * - Redistributions in binary form must reproduce the above copyright notice, 14 | * this list of conditions and the following disclaimer in the documentation 15 | * and/or other materials provided with the distribution. 16 | * 17 | * - Neither the name of the copyright holder nor the names of its 18 | * contributors may be used to endorse or promote products derived from 19 | * this software without specific prior written permission. 20 | * 21 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | **/ 32 | @IsTest 33 | private class fflib_DomainObjectBuilderTest 34 | { 35 | /** 36 | * @description Flag passed to helper methods to determine which test to run 37 | **/ 38 | private enum MethodMode { Build, BuildNew, Persist } 39 | 40 | /** 41 | * @description UnitOfWork SObjectTypes used for persist methods 42 | **/ 43 | private static List UOW_SOBJECTTYPES = 44 | new Schema.SObjectType[] { 45 | Account.SObjectType, 46 | Contact.SObjectType, 47 | Product2.SObjectType, 48 | PriceBook2.SObjectType, 49 | PriceBookEntry.SObjectType, 50 | Opportunity.SObjectType, 51 | OpportunityLineItem.SObjectType }; 52 | 53 | private static void runBuild(Boolean buildNew) 54 | { 55 | // Given 56 | TestAccountBuilder acctBuilder = anAccount(); 57 | Account beforeBuildRecord = acctBuilder.Record; // SObject available on builder instantiation 58 | 59 | // When 60 | Account acct = buildNew ? acctBuilder.buildNew() : acctBuilder.build(); 61 | 62 | // Then 63 | System.assertEquals(true, acctBuilder.IsBuilt); 64 | System.assertEquals(false, acctBuilder.IsRegistered); 65 | System.assertNotEquals(null, acct); 66 | System.assertNotEquals(null, acctBuilder.Record); 67 | System.assertEquals(beforeBuildRecord, acctBuilder.Record); // should be same SObject 68 | System.assertEquals(acct, acctBuilder.Record); // should be same SObject 69 | if (buildNew) { 70 | System.assertEquals(null, acctBuilder.Record.Id); 71 | System.assertEquals(null, acct.Id); 72 | } else { 73 | System.assertNotEquals(null, acctBuilder.Record.Id); 74 | System.assertNotEquals(null, acct.Id); 75 | } 76 | System.assertEquals(2, acctBuilder.getDomainObjectBuilderEventsFired().size()); 77 | System.assertEquals(buildNew, acctBuilder.getDomainObjectBuilderEventsFired().get('beforeBuild')); 78 | System.assertEquals(acct, acctBuilder.getDomainObjectBuilderEventsFired().get('afterBuild')); 79 | } 80 | 81 | private static void runBuildTwice(Boolean buildNewFirstCall, Boolean buildNewSecondCall) 82 | { 83 | // Given 84 | TestAccountBuilder acctBuilder = anAccount(); 85 | Account acct = buildNewFirstCall ? acctBuilder.buildNew() : acctBuilder.build(); 86 | 87 | // When 88 | fflib_DomainObjectBuilder.DomainObjectBuilderException caughtEx; 89 | try { 90 | Account acct2 = buildNewSecondCall ? acctBuilder.buildNew() : acctBuilder.build(); 91 | } catch (fflib_DomainObjectBuilder.DomainObjectBuilderException ex) { 92 | caughtEx = ex; 93 | } 94 | 95 | // Then 96 | System.assertNotEquals(null, caughtEx, 'Expected exception'); 97 | System.assertEquals('Builder has already been built', caughtEx.getMessage()); 98 | } 99 | 100 | private static void runBuildWithValues(Boolean buildNew) 101 | { 102 | // Given 103 | TestAccountBuilder acctBuilder = anAccount().withName('My Account'); 104 | TestContactBuilder contactBuilder = aContact().withLastName('My Contact').withAccount(acctBuilder); 105 | 106 | // When 107 | Contact contact = buildNew ? contactBuilder.buildNew() : contactBuilder.build(); 108 | 109 | // Then 110 | System.assertNotEquals(null, acctBuilder.Record.Id); 111 | if (buildNew) { 112 | System.assertEquals(null, contactBuilder.Record.Id); 113 | } else { 114 | System.assertNotEquals(null, contactBuilder.Record.Id); 115 | } 116 | System.assertEquals(contact.LastName, 'My Contact'); 117 | System.assertEquals(contactBuilder.Record.LastName, 'My Contact'); 118 | System.assertEquals(acctBuilder.Record.Name, 'My Account'); 119 | System.assertEquals(contact.AccountId, acctBuilder.Record.Id); 120 | 121 | } 122 | 123 | private static void runBuildAfterRegistering(Boolean buildNew) 124 | { 125 | // Given 126 | TestAccountBuilder acctBuilder = anAccount(); 127 | acctBuilder.register(); 128 | 129 | // When 130 | fflib_DomainObjectBuilder.DomainObjectBuilderException caughtEx; 131 | try { 132 | Account acct = buildNew ? acctBuilder.buildNew() : acctBuilder.build(); 133 | } catch (fflib_DomainObjectBuilder.DomainObjectBuilderException ex) { 134 | caughtEx = ex; 135 | } 136 | 137 | // Then 138 | System.assertNotEquals(null, caughtEx, 'Expected exception'); 139 | System.assertEquals('Builder has already been registered', caughtEx.getMessage()); 140 | } 141 | 142 | private static void runBuildWithRelationshipThatWasBuiltNew(Boolean buildNew) 143 | { 144 | // Given 145 | TestAccountBuilder accountBuilder = anAccount(); 146 | Account account = accountBuilder.buildNew(); 147 | TestContactBuilder testContactBuilder = aContact().withAccount(accountBuilder); 148 | 149 | // When 150 | fflib_DomainObjectBuilder.DomainObjectBuilderException caughtEx; 151 | try { 152 | Contact contact = buildNew ? testContactBuilder.buildNew() : testContactBuilder.build(); 153 | } catch (fflib_DomainObjectBuilder.DomainObjectBuilderException ex) { 154 | caughtEx = ex; 155 | } 156 | 157 | // Then 158 | System.assertNotEquals(null, caughtEx, 'Expected exception'); 159 | System.assertEquals(String.format('Field {0} contains value for a builder that was built as new', new List { Contact.AccountId.getDescribe().getName() }), caughtEx.getMessage()); 160 | } 161 | 162 | private static void runRegisterAfterBuilding(Boolean buildNew) 163 | { 164 | // Given 165 | TestAccountBuilder acctBuilder = anAccount(); 166 | Account acct = buildNew ? acctBuilder.buildNew() : acctBuilder.build(); 167 | 168 | // When 169 | fflib_DomainObjectBuilder.DomainObjectBuilderException caughtEx; 170 | try { 171 | acctBuilder.register(); 172 | } catch (fflib_DomainObjectBuilder.DomainObjectBuilderException ex) { 173 | caughtEx = ex; 174 | } 175 | 176 | // Then 177 | System.assertNotEquals(null, caughtEx, 'Expected exception'); 178 | System.assertEquals('Builder has already been built', caughtEx.getMessage()); 179 | } 180 | 181 | private static void runBuildWithRegisteredRelationship(Boolean buildNew) 182 | { 183 | // Given 184 | TestAccountBuilder acctBuilder = anAccount().register(); 185 | TestContactBuilder contactBuilder = aContact().withAccount(acctBuilder); 186 | 187 | // When 188 | fflib_DomainObjectBuilder.DomainObjectBuilderException caughtEx; 189 | try { 190 | Contact contact = buildNew ? contactBuilder.buildNew() : contactBuilder.build(); 191 | } catch (fflib_DomainObjectBuilder.DomainObjectBuilderException ex) { 192 | caughtEx = ex; 193 | } 194 | 195 | // Then 196 | System.assertNotEquals(null, caughtEx, 'Expected exception'); 197 | System.assertEquals(String.format('Field {0} contains value for a builder that is marked for registration', new List { Contact.AccountId.getDescribe().getName() }), caughtEx.getMessage()); 198 | } 199 | 200 | private static void verifySetThrowsException(TestContactBuilder contactBuilder, Boolean useRelationshipField) 201 | { 202 | // When 203 | fflib_DomainObjectBuilder.DomainObjectBuilderException caughtEx; 204 | try { 205 | TestContactBuilder builder = useRelationshipField ? contactBuilder.withAccount(anAccount()) : contactBuilder.withLastName('TestLastName'); 206 | } catch (fflib_DomainObjectBuilder.DomainObjectBuilderException ex) { 207 | caughtEx = ex; 208 | } 209 | 210 | // Then 211 | System.assertNotEquals(null, caughtEx, 'Expected exception'); 212 | System.assertEquals('Builder has already been built', caughtEx.getMessage()); 213 | } 214 | 215 | private static void runPersistWithRelationshipThatWasBuilt(MethodMode mode) 216 | { 217 | // Given 218 | TestAccountBuilder accountBuilder = anAccount().withName('Test Account'); 219 | if (MethodMode.Persist == mode) { 220 | accountBuilder.register(); 221 | fflib_DomainObjectBuilder.persistRegistered(new fflib_SObjectUnitOfWork(UOW_SOBJECTTYPES)); 222 | } else { 223 | Account account = (MethodMode.BuildNew == mode) ? accountBuilder.buildNew() : accountBuilder.build(); 224 | } 225 | TestContactBuilder testContactBuilder = aContact().withLastName('Test Contact').withAccount(accountBuilder).register(); 226 | 227 | // When 228 | fflib_DomainObjectBuilder.DomainObjectBuilderException caughtEx; 229 | try { 230 | fflib_DomainObjectBuilder.persistRegistered(new fflib_SObjectUnitOfWork(UOW_SOBJECTTYPES)); 231 | } catch (fflib_DomainObjectBuilder.DomainObjectBuilderException ex) { 232 | caughtEx = ex; 233 | } 234 | 235 | // Then 236 | System.assertNotEquals(null, caughtEx, 'Expected exception'); 237 | System.assertEquals(String.format('Field {0} contains value for a builder that has already been built', new List { Contact.AccountId.getDescribe().getName() }), caughtEx.getMessage()); 238 | } 239 | 240 | private static void runBuildRelationshipTree(Boolean buildNew) 241 | { 242 | // Given 243 | TestAccountBuilder accountBuilder1 = anAccount().withName('Test Account1'); 244 | TestAccountBuilder accountBuilder2 = anAccount().withName('Test Account2'); 245 | TestOpportunityBuilder opportunityBuilder1 = aClosedWonOpportunity().withAccount(accountBuilder2); 246 | TestOpportunityBuilder opportunityBuilder2 = opportunityBuilder1.but().withName('Built with but').withAccount(accountBuilder1); 247 | TestContactBuilder contactBuilder1 = aContact().withLastName('Jones').withAccount(accountBuilder1); 248 | TestContactBuilder contactBuilder2 = contactBuilder1.but().withLastName('Smith'); 249 | 250 | // When 251 | Opportunity opportunity1 = buildNew ? opportunityBuilder1.buildNew() : opportunityBuilder1.build(); 252 | Opportunity opportunity2 = buildNew ? opportunityBuilder2.buildNew() : opportunityBuilder2.build(); 253 | Contact contact1 = buildNew ? contactBuilder1.buildNew() : contactBuilder1.build(); 254 | Contact contact2 = buildNew ? contactBuilder2.buildNew() : contactBuilder2.build(); 255 | 256 | // Then 257 | Account account1 = accountBuilder1.Record; 258 | Account account2 = accountBuilder2.Record; 259 | verifyRelationshipTree(account1, account2, contact1, contact2, opportunity1, opportunity2, buildNew); 260 | } 261 | 262 | private static SObject assertAndGet(Map records, Id searchForId) 263 | { 264 | System.assertNotEquals(null, searchForId); 265 | System.assertEquals(true, records.containsKey(searchForId)); 266 | return records.get(searchForId); 267 | } 268 | 269 | private static void verifyRelationshipTree(Account account1, Account account2, Contact contact1, Contact contact2, Opportunity opportunity1, Opportunity opportunity2, Boolean buildNew) 270 | { 271 | System.assertNotEquals(null, account1.Id); 272 | System.assertNotEquals(null, account2.Id); 273 | System.assert(buildNew ? (null == opportunity1.Id) : (null != opportunity1.Id), 'Value for Opportunity1 Id was not expected'); 274 | System.assert(buildNew ? (null == opportunity2.Id) : (null != opportunity2.Id), 'Value for Opportunity2 Id was not expected'); 275 | System.assert(buildNew ? (opportunity1.Id == opportunity2.Id) : (opportunity1.Id != opportunity2.Id), 'Opportunity1 Id and Opportunity2 Id mismatch'); 276 | System.assertEquals('Closed Won', opportunity1.StageName); 277 | System.assertEquals('Built with but', opportunity2.Name); 278 | System.assertNotEquals(opportunity1.Name, opportunity2.Name); 279 | System.assertNotEquals(null, opportunity1.AccountId); 280 | System.assertNotEquals(null, opportunity2.AccountId); 281 | System.assertNotEquals(opportunity1.AccountId, opportunity2.AccountId); 282 | System.assert(buildNew ? (null == contact1.Id) : (null != contact1.Id), 'Value for Contact1 Id was not expected'); 283 | System.assert(buildNew ? (null == contact2.Id) : (null != contact2.Id), 'Value for Contact2 Id was not expected'); 284 | System.assert(buildNew ? (contact1.Id == contact2.Id) : (contact1.Id != contact2.Id), 'Contact1 Id and Contact2 Id mismatch'); 285 | System.assertEquals('Jones', contact1.LastName); 286 | } 287 | 288 | private static void runDeepRelationshipTree(MethodMode mode) 289 | { 290 | // Given 291 | TestProductBuilder prdBuilder = aProduct() 292 | .withName('My Test Product'); 293 | 294 | TestPriceBookEntryBuilder pbeBuilder = aPriceBookEntryWithStandardPriceBook() 295 | .withUnitPrice(200) 296 | .withIsActive(true) 297 | .withProduct(prdBuilder); 298 | 299 | TestOpportunityLineItemBuilder oliBuilder = anOpportunityLineItem() 300 | .withQuantity(5) 301 | .withTotalPrice(500) 302 | .withPriceBookEntry( 303 | aPriceBookEntry() 304 | .withUnitPrice(100) 305 | .withIsActive(true) 306 | .withUseStandardPrice(false) 307 | .withProduct(prdBuilder) 308 | .withPriceBook( 309 | aPriceBook() 310 | .withName('My PriceBook') 311 | .withIsActive(true))) 312 | .withOpportunity( 313 | anOpportunity() 314 | .withName('My Test Opportunity') 315 | .withStageName('Prospecting') 316 | .withCloseDate(System.today()) 317 | .withAccount( 318 | anAccount() 319 | .withName('My Test Account'))); 320 | 321 | if (MethodMode.Persist == mode) { 322 | // When 323 | pbeBuilder.register(); 324 | oliBuilder.register(); 325 | fflib_DomainObjectBuilder.persistRegistered(new fflib_SObjectUnitOfWork(UOW_SOBJECTTYPES)); 326 | 327 | // Then 328 | System.assertEquals(1, [SELECT COUNT() FROM Opportunity]); 329 | System.assertEquals(1, [SELECT COUNT() FROM OpportunityLineItem]); 330 | System.assertEquals(1, [SELECT COUNT() FROM PriceBook2]); 331 | System.assertEquals(2, [SELECT COUNT() FROM PriceBookEntry]); 332 | System.assertEquals(1, [SELECT COUNT() FROM Product2]); 333 | System.assertEquals(1, [SELECT COUNT() FROM Account]); 334 | 335 | } else { 336 | // When 337 | PriceBookEntry pbe = MethodMode.BuildNew == mode ? pbeBuilder.buildNew() : pbeBuilder.build(); 338 | OpportunityLineItem oli = MethodMode.BuildNew == mode ? oliBuilder.buildNew() : oliBuilder.build(); 339 | 340 | // Then 341 | System.assertNotEquals(null, prdBuilder.Record.Id); 342 | System.assert(MethodMode.BuildNew == mode ? pbe.Id == null : pbe.Id != null, 'Value for PriceBookEntry Id was not expected'); 343 | System.assertEquals(prdBuilder.Record.Id, pbe.Product2Id); 344 | System.assert(MethodMode.BuildNew == mode ? oli.Id == null : null != oli.Id, 'Value for OpportunityLineItem Id was not expected'); 345 | System.assertNotEquals(null, oli.OpportunityId); 346 | System.assertNotEquals(null, oli.PriceBookEntryId); 347 | } 348 | } 349 | 350 | /** 351 | * @description Confirms that creating a builder has the correct initial state 352 | **/ 353 | @IsTest 354 | private static void testCreateBuilderInstance() 355 | { 356 | // Given 357 | TestAccountBuilder acctBuilder = anAccount(); 358 | 359 | // When 360 | 361 | // Then 362 | System.assertNotEquals(null, acctBuilder.Record); // does not require build to have SObject instance created 363 | System.assertEquals(false, acctBuilder.IsBuilt); 364 | System.assertEquals(false, acctBuilder.IsRegistered); 365 | System.assertEquals(null, acctBuilder.Record.Id); 366 | System.assertEquals(Account.SObjectType, acctBuilder.getSObjectType()); 367 | } 368 | 369 | /** 370 | * @description Confirms that calling build results with a valid SObject that has an Id 371 | **/ 372 | @IsTest 373 | private static void testBuild() 374 | { 375 | runBuild(false); 376 | } 377 | 378 | /** 379 | * @description Confirms that calling buildNew results with a valid SObject that does NOT have an Id 380 | **/ 381 | @IsTest 382 | private static void testBuildNew() 383 | { 384 | runBuild(true); 385 | } 386 | 387 | /** 388 | * @description Confirms that calling register results in the builder having the correct state 389 | **/ 390 | @IsTest 391 | private static void testRegister() 392 | { 393 | // Given 394 | TestAccountBuilder acctBuilder = anAccount(); 395 | Account beforeBuildRecord = acctBuilder.Record; 396 | 397 | // When 398 | acctBuilder.register(); 399 | 400 | // Then 401 | System.assertEquals(false, acctBuilder.IsBuilt); 402 | System.assertEquals(true, acctBuilder.IsRegistered); 403 | System.assertNotEquals(null, acctBuilder.Record); 404 | System.assertEquals(null, acctBuilder.Record.Id); 405 | } 406 | 407 | /** 408 | * @description Confirms that a builder cannot be built twice 409 | **/ 410 | @IsTest 411 | private static void testBuildingTwiceThrowsException() 412 | { 413 | // build and build 414 | runBuildTwice(false, false); 415 | // build and buildNew 416 | runBuildTwice(false, true); 417 | // buildNew and buildNew 418 | runBuildTwice(true, true); 419 | // buildNew and build 420 | runBuildTwice(true, false); 421 | } 422 | 423 | /** 424 | * @description Confirms that a builder cannot be built if it is registered 425 | **/ 426 | @IsTest 427 | private static void testBuildAfterRegisteringThrowsException() 428 | { 429 | runBuildAfterRegistering(false); 430 | } 431 | 432 | /** 433 | * @description Confirms that a builder cannot be builtnew if it is registered 434 | **/ 435 | @IsTest 436 | private static void testBuildNewAfterRegisteringThrowsException() 437 | { 438 | runBuildAfterRegistering(true); 439 | runBuildAfterRegistering(false); 440 | } 441 | 442 | /** 443 | * @description Confirms that a builder cannot be registered if it has been built 444 | **/ 445 | @IsTest 446 | private static void testRegisteringAfterBuildThrowsException() 447 | { 448 | runRegisterAfterBuilding(false); 449 | runRegisterAfterBuilding(true); 450 | } 451 | 452 | /** 453 | * @description Confirms that a builder cannot be registered twice 454 | **/ 455 | @IsTest 456 | private static void testRegisteringTwiceThrowsException() 457 | { 458 | // Given 459 | TestAccountBuilder acctBuilder = anAccount(); 460 | 461 | // When 462 | acctBuilder.register(); 463 | 464 | // Then 465 | System.assertEquals(true, acctBuilder.IsRegistered); 466 | System.assertEquals(false, acctBuilder.IsBuilt); 467 | System.assertEquals(null, acctBuilder.Record.Id); 468 | 469 | // When 470 | fflib_DomainObjectBuilder.DomainObjectBuilderException caughtEx; 471 | try { 472 | acctBuilder.register(); 473 | } catch (fflib_DomainObjectBuilder.DomainObjectBuilderException ex) { 474 | caughtEx = ex; 475 | } 476 | 477 | // Then 478 | System.assertNotEquals(null, caughtEx, 'Expected exception'); 479 | System.assertEquals('Builder has already been registered', caughtEx.getMessage()); 480 | } 481 | 482 | /** 483 | * @description Confirms that a builder field data cannot be changed after it has been built 484 | **/ 485 | @IsTest 486 | private static void testChangingDataAfterBuildThrowsException() 487 | { 488 | // Given 489 | TestContactBuilder contactBuilder = aContact(); 490 | Contact contact = contactBuilder.build(); 491 | 492 | // When and Then - Non relationship field 493 | verifySetThrowsException(contactBuilder, false); 494 | // When and Then - relationship field 495 | verifySetThrowsException(contactBuilder, true); 496 | } 497 | 498 | /** 499 | * @description Confirms that a builder field data cannot be changed after it has been builtnew 500 | **/ 501 | @IsTest 502 | private static void testChangingDataAfterBuildNewThrowsException() 503 | { 504 | // Given 505 | TestContactBuilder contactBuilder = aContact(); 506 | Contact contact = contactBuilder.buildNew(); 507 | 508 | // When and Then - Non relationship field 509 | verifySetThrowsException(contactBuilder, false); 510 | // When and Then - relationship field 511 | verifySetThrowsException(contactBuilder, true); 512 | } 513 | 514 | /** 515 | * @description Confirms that a builder field data cannot be changed after it has been persisted 516 | **/ 517 | @IsTest 518 | private static void testChangingDataAfterPersistThrowsException() 519 | { 520 | // Given 521 | TestContactBuilder contactBuilder = aContact().withLastName('Test Contact').register(); 522 | fflib_DomainObjectBuilder.persistRegistered(new fflib_SObjectUnitOfWork(UOW_SOBJECTTYPES)); 523 | 524 | // When and Then - Non relationship field 525 | verifySetThrowsException(contactBuilder, false); 526 | // When and Then - relationship field 527 | verifySetThrowsException(contactBuilder, true); 528 | } 529 | 530 | /** 531 | * @description Confirms that a builder that contains a relationship field that references a builder 532 | * that has been registered cannot be built 533 | **/ 534 | @IsTest 535 | private static void testBuildWhenRelationshipIsRegisteredThrowsException() 536 | { 537 | // build 538 | runBuildWithRegisteredRelationship(false); 539 | // buildNew 540 | runBuildWithRegisteredRelationship(true); 541 | } 542 | 543 | /** 544 | * @description Confirms that calling persistRegistered results with a valid SObject in the database 545 | **/ 546 | @IsTest 547 | private static void testPersistRegistered() 548 | { 549 | // Given 550 | Integer numAccounts = 5; 551 | List builders = new List(); 552 | for (Integer i = 0; i < numAccounts; i++) { 553 | builders.add(anAccount().withName('Account' + i).register()); 554 | } 555 | 556 | // When 557 | fflib_DomainObjectBuilder.persistRegistered(new fflib_SObjectUnitOfWork(UOW_SOBJECTTYPES)); 558 | 559 | // Then 560 | Map accounts = new Map([SELECT Id, Name FROM Account]); 561 | System.assertEquals(builders.size(), accounts.size()); 562 | for (Integer i = 0; i < numAccounts; i++) { 563 | TestAccountBuilder builder = builders[i]; 564 | System.assertEquals(true, builder.IsBuilt); 565 | System.assertEquals(false, builder.IsRegistered); 566 | System.assertNotEquals(null, builder.Record.Id); 567 | System.assert(accounts.containsKey(builder.Record.Id), 'Builder Id ' + builder.Record.Id + ' not found in database'); 568 | System.assertEquals(builder.Record, accounts.get(builder.Record.Id)); 569 | System.assertEquals('Account' + i, accounts.get(builder.Record.Id).Name); 570 | System.assertEquals(2, builder.getDomainObjectBuilderEventsFired().size()); 571 | System.assertEquals(builder.Record, builder.getDomainObjectBuilderEventsFired().get('beforeInsert')); 572 | System.assertEquals(builder.Record, builder.getDomainObjectBuilderEventsFired().get('afterInsert')); 573 | } 574 | } 575 | 576 | /** 577 | * @description Confirms that building results in all fields containing correct values 578 | **/ 579 | @IsTest 580 | private static void testBuildingWithValuesHasCorrectValues() 581 | { 582 | runBuildWithValues(false); 583 | runBuildWithValues(true); 584 | } 585 | 586 | /** 587 | * @description Confirms that persisting results in all fields containing correct values 588 | **/ 589 | @IsTest 590 | private static void testPersistingWithValuesHasCorrectValues() 591 | { 592 | // Given 593 | Integer numAccounts = 5; 594 | List accountBuilders = new List(); 595 | List contactBuilders = new List(); 596 | for (Integer i = 0; i < numAccounts; i++) { 597 | TestAccountBuilder accountBuilder = anAccount().withName('My Account' + i).register(); 598 | TestContactBuilder contactBuilder = aContact().withLastName('My Contact' + i).withAccount(accountBuilder).register(); 599 | accountBuilders.add(accountBuilder); 600 | contactBuilders.add(contactBuilder); 601 | } 602 | 603 | // When 604 | fflib_DomainObjectBuilder.persistRegistered(new fflib_SObjectUnitOfWork(UOW_SOBJECTTYPES)); 605 | 606 | // Then 607 | Map accounts = new Map([SELECT Id, Name FROM Account]); 608 | Map contacts = new Map([SELECT Id, LastName, AccountId FROM Contact]); 609 | System.assertEquals(numAccounts, accounts.size()); 610 | System.assertEquals(numAccounts, contacts.size()); 611 | for (Integer i = 0; i < numAccounts; i++) { 612 | TestAccountBuilder accountBuilder = accountBuilders[i]; 613 | TestContactBuilder contactBuilder = contactBuilders[i]; 614 | Account account = accountBuilder.Record; 615 | Contact contact = contactBuilder.Record; 616 | 617 | System.assertEquals(true, accounts.containsKey(account.Id)); 618 | System.assertEquals(account, accounts.get(account.Id)); 619 | System.assertEquals('My Account' + i, accounts.get(account.Id).Name); 620 | System.assertEquals(true, contacts.containsKey(contact.Id)); 621 | System.assertEquals(contact, contacts.get(contact.Id)); 622 | System.assertEquals('My Contact' + i, contacts.get(contact.Id).LastName); 623 | System.assertEquals(account.Id, contact.AccountId); 624 | } 625 | } 626 | 627 | /** 628 | * @description Confirms that copy constructor (clone) results in a builder instance 629 | * with the same exact field values as the source instance 630 | **/ 631 | @IsTest 632 | private static void testClone() 633 | { 634 | // Given 635 | TestAccountBuilder accountBuilder = anAccount().withName('Test Account'); 636 | 637 | // When 638 | TestAccountBuilder clonedAccountBuilder = accountBuilder.but(); 639 | 640 | // Then 641 | clonedAccountBuilder.assertEquals(accountBuilder); 642 | 643 | // When 644 | Account account = accountBuilder.build(); 645 | Account clonedAccount = clonedAccountBuilder.withName('Test Account2').build(); 646 | 647 | // Then 648 | System.assertEquals('Test Account', account.Name); 649 | System.assertEquals('Test Account2', clonedAccount.Name); 650 | } 651 | 652 | /** 653 | * @description Confirms that builder that contains a field value to another builder 654 | * that has been built can be built 655 | **/ 656 | @IsTest 657 | private static void testBuildWithRelationshipThatIsAlreadyBuilt() 658 | { 659 | // Build Account after estaliblishing relationship 660 | // Given 661 | TestAccountBuilder accountBuilder = anAccount(); 662 | TestContactBuilder contactBuilder = aContact().withAccount(accountBuilder); 663 | Account account = accountBuilder.build(); 664 | 665 | // When 666 | Contact contact = contactBuilder.build(); 667 | 668 | // Then 669 | System.assertNotEquals(null, contact.AccountId); 670 | System.assertEquals(contact.AccountId, account.Id); 671 | 672 | // Before Account before establishing relationship 673 | // Given 674 | TestAccountBuilder accountBuilder2 = anAccount(); 675 | Account account2 = accountBuilder2.build(); 676 | TestContactBuilder contactBuilder2 = aContact().withAccount(accountBuilder2); 677 | 678 | // When 679 | Contact contact2 = contactBuilder2.build(); 680 | 681 | // Then 682 | System.assertNotEquals(null, contact2.AccountId); 683 | System.assertEquals(contact2.AccountId, account2.Id); 684 | } 685 | 686 | /** 687 | * @description Confirms that builder that contains a field value to another builder 688 | * that has been builtnew cannot be built 689 | **/ 690 | @IsTest 691 | private static void testBuildWithRelationshipThatWasBuiltNewThrowsException() 692 | { 693 | runBuildWithRelationshipThatWasBuiltNew(false); 694 | runBuildWithRelationshipThatWasBuiltNew(true); 695 | } 696 | 697 | /** 698 | * @description Confirms that builder that contains a field value to another builder 699 | * that has been built cannot be persisted 700 | **/ 701 | @IsTest 702 | private static void testPersistWithRelationshipThatWasBuiltThrowsException() 703 | { 704 | runPersistWithRelationshipThatWasBuilt(MethodMode.Build); 705 | } 706 | 707 | /** 708 | * @description Confirms that builder that contains a field value to another builder 709 | * that has been builtnew cannot be persisted 710 | **/ 711 | @IsTest 712 | private static void testPersistWithRelationshipThatWasBuiltNewThrowsException() 713 | { 714 | runPersistWithRelationshipThatWasBuilt(MethodMode.BuildNew); 715 | } 716 | 717 | /** 718 | * @description Confirms that builder that contains a field value to another builder 719 | * that has been persisted cannot be persisted 720 | **/ 721 | @IsTest 722 | private static void testPeristWithRelationshipThatWasPersistedThrowsException() 723 | { 724 | runPersistWithRelationshipThatWasBuilt(MethodMode.Persist); 725 | } 726 | 727 | /** 728 | * @description Confirms that builder can be persisted when one of its fields 729 | * references another builder that has not been registered 730 | **/ 731 | @IsTest 732 | private static void testPersistWithRelationshipThatIsNotRegistered() 733 | { 734 | // Given 735 | TestAccountBuilder accountBuilder = anAccount().withName('Test Account'); 736 | TestContactBuilder contactBuilder = aContact().withLastName('Test Contact').withAccount(accountBuilder).register(); 737 | 738 | // When 739 | fflib_DomainObjectBuilder.persistRegistered(new fflib_SObjectUnitOfWork(UOW_SOBJECTTYPES)); 740 | 741 | // Then 742 | List accounts = [SELECT Id, Name FROM Account]; 743 | List contacts = [SELECT Id, LastName, AccountId FROM Contact]; 744 | System.assertEquals(1, accounts.size()); 745 | System.assertEquals(1, contacts.size()); 746 | System.assertEquals(accounts[0].Id, contacts[0].AccountId); 747 | System.assertEquals(2, accountBuilder.getDomainObjectBuilderEventsFired().size()); 748 | System.assertEquals(accountBuilder.Record, accountBuilder.getDomainObjectBuilderEventsFired().get('beforeInsert')); 749 | System.assertEquals(accountBuilder.Record, accountBuilder.getDomainObjectBuilderEventsFired().get('afterInsert')); 750 | System.assertEquals(2, contactBuilder.getDomainObjectBuilderEventsFired().size()); 751 | System.assertEquals(contactBuilder.Record, contactBuilder.getDomainObjectBuilderEventsFired().get('beforeInsert')); 752 | System.assertEquals(contactBuilder.Record, contactBuilder.getDomainObjectBuilderEventsFired().get('afterInsert')); 753 | } 754 | 755 | /** 756 | * @description Confirms that multiple builders with several relationships can be persisted 757 | **/ 758 | @IsTest 759 | private static void testPersistRegisteredRelationshipTree() 760 | { 761 | // Given 762 | TestAccountBuilder accountBuilder1 = anAccount().withName('Test Account1'); 763 | TestAccountBuilder accountBuilder2 = anAccount().withName('Test Account2'); 764 | TestOpportunityBuilder opportunityBuilder1 = aClosedWonOpportunity().withAccount(accountBuilder2).register(); 765 | TestOpportunityBuilder opportunityBuilder2 = opportunityBuilder1.but().withName('Built with but').withAccount(accountBuilder1).register(); 766 | TestContactBuilder contactBuilder1 = aContact().withLastName('Jones').withAccount(accountBuilder1).register(); 767 | TestContactBuilder contactBuilder2 = contactBuilder1.but().withLastName('Smith').register(); 768 | 769 | // When 770 | fflib_DomainObjectBuilder.persistRegistered(new fflib_SObjectUnitOfWork(UOW_SOBJECTTYPES)); 771 | 772 | // Then 773 | Map accounts = new Map([SELECT Id, Name FROM Account]); 774 | System.assertEquals(2, accounts.size()); 775 | Map contacts = new Map([SELECT Id, LastName, AccountId FROM Contact]); 776 | System.assertEquals(2, contacts.size()); 777 | Map opportunities = new Map([SELECT Id, Name, StageName, AccountId FROM Opportunity]); 778 | System.assertEquals(2, opportunities.size()); 779 | Account account1 = (Account)assertAndGet(accounts, accountBuilder1.Record.Id); 780 | Account account2 = (Account)assertAndGet(accounts, opportunityBuilder1.Record.AccountId); 781 | Contact contact1 = (Contact)assertAndGet(contacts, contactBuilder1.Record.Id); 782 | Contact contact2 = (Contact)assertAndGet(contacts, contactBuilder2.Record.Id); 783 | Opportunity opportunity1 = (Opportunity)assertAndGet(opportunities, opportunityBuilder1.Record.Id); 784 | Opportunity opportunity2 = (Opportunity)assertAndGet(opportunities, opportunityBuilder2.Record.Id); 785 | verifyRelationshipTree(account1, account2, contact1, contact2, opportunity1, opportunity2, false); 786 | } 787 | 788 | /** 789 | * @description Confirms that multiple builders with several relationships can be built 790 | **/ 791 | @IsTest 792 | private static void testBuildRelationshipTree() 793 | { 794 | runBuildRelationshipTree(false); 795 | runBuildRelationshipTree(true); 796 | } 797 | 798 | /** 799 | * @description Confirms that multiple builders with several layers of relationships can be built 800 | **/ 801 | @IsTest 802 | private static void testBuildDeepRelationshipTree() 803 | { 804 | runDeepRelationshipTree(MethodMode.Build); 805 | runDeepRelationshipTree(MethodMode.BuildNew); 806 | } 807 | 808 | /** 809 | * @description Confirms that multiple builders with several layers of relationships can be persisted 810 | **/ 811 | @IsTest 812 | private static void testPersistDeepRelationshipTree() 813 | { 814 | runDeepRelationshipTree(MethodMode.Persist); 815 | } 816 | 817 | /** 818 | * @description Confirms that a builder instance can be persisted without being registered 819 | **/ 820 | @IsTest 821 | private static void testPersistBuilder() 822 | { 823 | // Given 824 | TestAccountBuilder accountBuilder = anAccount().withName('Test Account'); 825 | 826 | // When 827 | Account account = accountBuilder.persist(new fflib_SObjectUnitOfWork(UOW_SOBJECTTYPES)); 828 | 829 | // Then 830 | Map accounts = new Map([SELECT Id, Name FROM Account]); 831 | System.assertEquals(true, accountBuilder.IsBuilt); 832 | System.assertEquals(false, accountBuilder.IsRegistered); 833 | System.assertNotEquals(null, accountBuilder.Record.Id); 834 | System.assertEquals(account.Id, accountBuilder.Record.Id); 835 | System.assertEquals(account, accountBuilder.Record); 836 | System.assertEquals('Test Account', account.Name); 837 | System.assertEquals(2, accountBuilder.getDomainObjectBuilderEventsFired().size()); 838 | System.assertEquals(accountBuilder.Record, accountBuilder.getDomainObjectBuilderEventsFired().get('beforeInsert')); 839 | System.assertEquals(accountBuilder.Record, accountBuilder.getDomainObjectBuilderEventsFired().get('afterInsert')); 840 | } 841 | 842 | /** 843 | * @description Confirms that a builder instance with several layers of relationships can be persisted without being registered 844 | **/ 845 | @IsTest 846 | private static void testPersistRelationshipTree() 847 | { 848 | TestOpportunityLineItemBuilder oliBuilder = anOpportunityLineItem() 849 | .withQuantity(5) 850 | .withTotalPrice(500) 851 | .withPriceBookEntry( 852 | aPriceBookEntryWithStandardPriceBook() 853 | .withUnitPrice(200) 854 | .withIsActive(true) 855 | .withProduct( 856 | aProduct() 857 | .withName('My Test Product'))) 858 | .withOpportunity( 859 | anOpportunity() 860 | .withName('My Test Opportunity') 861 | .withStageName('Prospecting') 862 | .withCloseDate(System.today()) 863 | .withAccount( 864 | anAccount() 865 | .withName('My Test Account'))); 866 | 867 | // When 868 | OpportunityLineItem oli = oliBuilder.persist(new fflib_SObjectUnitOfWork(UOW_SOBJECTTYPES)); 869 | 870 | // Then 871 | System.assertEquals(1, [SELECT COUNT() FROM Opportunity]); 872 | System.assertEquals(1, [SELECT COUNT() FROM OpportunityLineItem]); 873 | System.assertEquals(1, [SELECT COUNT() FROM PriceBookEntry]); 874 | System.assertEquals(1, [SELECT COUNT() FROM Product2]); 875 | System.assertEquals(1, [SELECT COUNT() FROM Account]); 876 | System.assertNotEquals(null, oli.Id); 877 | } 878 | 879 | /** 880 | * @description Object Mother method for an empty account 881 | * 882 | * @remarks This would normally go within the TestAccountBuilder itself 883 | * but static methods are not allowed on inner classes. 884 | **/ 885 | private static TestAccountBuilder anAccount() { 886 | return new TestAccountBuilder(); 887 | } 888 | 889 | /** 890 | * @description Object Mother method for an Prospect account 891 | * 892 | * @remarks This would normally go within the TestAccountBuilder itself 893 | * but static methods are not allowed on inner classes. 894 | **/ 895 | private static TestAccountBuilder aProspect() { 896 | return anAccount().withName('Potential Customer').withType('Prospect'); 897 | } 898 | 899 | /** 900 | * @description Object Mother method for an Empty contact 901 | * 902 | * @remarks This would normally go within the TestContactBuilder itself 903 | * but static methods are not allowed on inner classes. 904 | **/ 905 | private static TestContactBuilder aContact() { 906 | return new TestContactBuilder(); 907 | } 908 | 909 | /** 910 | * @description Object Mother method for a Contact with an Account 911 | * 912 | * @remarks This would normally go within the TestContactBuilder itself 913 | * but static methods are not allowed on inner classes. 914 | **/ 915 | private static TestContactBuilder aContactWithAccount() { 916 | return aContact().withLastName('Test Contact').withAccount(anAccount().withName('Test Account')); 917 | } 918 | 919 | /** 920 | * @description Object Mother method for an Empty Opportunity 921 | * 922 | * @remarks This would normally go within the TestOpportunityBuilder itself 923 | * but static methods are not allowed on inner classes. 924 | **/ 925 | private static TestOpportunityBuilder anOpportunity() { 926 | return new TestOpportunityBuilder(); 927 | } 928 | 929 | /** 930 | * @description Object Mother method for a Closed Won Opportunity 931 | * 932 | * @remarks This would normally go within the TestOpportunityBuilder itself 933 | * but static methods are not allowed on inner classes. 934 | **/ 935 | private static TestOpportunityBuilder aClosedWonOpportunity() { 936 | return anOpportunity() 937 | .withName('Large Purchase') 938 | .withAccount(aProspect()) 939 | .withAmount(1000000.00) 940 | .withStageName('Closed Won') 941 | .withType('New Customer') 942 | .withCloseDate(System.today()); 943 | } 944 | 945 | /** 946 | * @description Object Mother method for an Empty Opportunity Line Item 947 | * 948 | * @remarks This would normally go within the TestOpportunityLineItemBuilder itself 949 | * but static methods are not allowed on inner classes. 950 | **/ 951 | private static TestOpportunityLineItemBuilder anOpportunityLineItem() { 952 | return new TestOpportunityLineItemBuilder(); 953 | } 954 | 955 | /** 956 | * @description Object Mother method for an Empty Price Book 957 | * 958 | * @remarks This would normally go within the TestPriceBookBuilder itself 959 | * but static methods are not allowed on inner classes. 960 | **/ 961 | private static TestPriceBookBuilder aPriceBook() { 962 | return new TestPriceBookBuilder(); 963 | } 964 | 965 | /** 966 | * @description Object Mother method for an Empty Price Book Entry 967 | * 968 | * @remarks This would normally go within the TestPriceBookEntryBuilder itself 969 | * but static methods are not allowed on inner classes. 970 | **/ 971 | private static TestPriceBookEntryBuilder aPriceBookEntry() { 972 | return new TestPriceBookEntryBuilder(); 973 | } 974 | 975 | /** 976 | * @description Object Mother method for Price Book Entry that uses the Standard Price Book 977 | * 978 | * @remarks This would normally go within the TestPriceBookEntryBuilder itself 979 | * but static methods are not allowed on inner classes. 980 | **/ 981 | private static TestPriceBookEntryBuilder aPriceBookEntryWithStandardPriceBook() { 982 | return aPriceBookEntry().withStandardPriceBook(); 983 | } 984 | 985 | /** 986 | * @description Object Mother method for an Empty Product 987 | * 988 | * @remarks This would normally go within the TestProductBuilder itself 989 | * but static methods are not allowed on inner classes. 990 | **/ 991 | private static TestProductBuilder aProduct() { 992 | return new TestProductBuilder(); 993 | } 994 | 995 | private abstract class TestBuilderBase extends fflib_DomainObjectBuilder 996 | { 997 | /** 998 | * @description Tracks events fired on this instance to aid in test verifications 999 | **/ 1000 | private Map m_domainObjectBuilderEvents = new Map(); 1001 | 1002 | protected TestBuilderBase(SObjectType type) { 1003 | super(type); 1004 | } 1005 | 1006 | protected TestBuilderBase(TestBuilderBase copyFrom) { 1007 | super(copyFrom); 1008 | } 1009 | 1010 | public Map getDomainObjectBuilderEventsFired() { 1011 | return m_domainObjectBuilderEvents.clone(); 1012 | } 1013 | 1014 | protected virtual override void beforeBuild(Boolean isNew) { 1015 | super.beforeBuild(isNew); 1016 | addEvent('beforeBuild', isNew); 1017 | } 1018 | 1019 | protected virtual override void afterBuild(SObject record) { 1020 | super.afterBuild(record); 1021 | addEvent('afterBuild', record); 1022 | } 1023 | 1024 | protected virtual override void beforeInsert(SObject record) { 1025 | super.beforeInsert(record); 1026 | addEvent('beforeInsert', record); 1027 | } 1028 | 1029 | protected virtual override void afterInsert(SObject record) { 1030 | super.afterInsert(record); 1031 | addEvent('afterInsert', record); 1032 | } 1033 | 1034 | protected void addEvent(String eventName, Object value) { 1035 | if (m_domainObjectBuilderEvents.containsKey(eventName)) { 1036 | throw new TestDomainBuilderException(String.format('Event {0} has already been fired.', new List { eventName })); 1037 | } 1038 | 1039 | m_domainObjectBuilderEvents.put(eventName, value); 1040 | } 1041 | 1042 | /** 1043 | * @description Helper to provide test support to gain access to protected members 1044 | **/ 1045 | protected void assertEquals(TestAccountBuilder compareTo) { 1046 | System.assertEquals(this.m_fieldValueMap, compareTo.m_fieldValueMap); 1047 | System.assertEquals(this.m_parentByRelationship, compareTo.m_parentByRelationship); 1048 | System.assertEquals(this.m_sObjType, compareTo.m_sObjType); 1049 | } 1050 | } 1051 | 1052 | private class TestAccountBuilder extends TestBuilderBase 1053 | { 1054 | /** 1055 | * Methods/Properties below would be included in a basic template for any derived builder class 1056 | * 1057 | * BEGIN STANDARD BUILDER TEMPLATE 1058 | * ------------------------------- 1059 | **/ 1060 | 1061 | /** 1062 | * @description Constructor should be included in every derived builder 1063 | **/ 1064 | private TestAccountBuilder() { 1065 | super(Account.SObjectType); 1066 | } 1067 | 1068 | /** 1069 | * @description Constructor should be included in every derived builder 1070 | **/ 1071 | private TestAccountBuilder(TestAccountBuilder copyFrom) { 1072 | super(copyFrom); 1073 | } 1074 | 1075 | /** 1076 | * @description Creates an existing SObject without issuing DML 1077 | * 1078 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1079 | **/ 1080 | public Account build() { 1081 | return (Account)build(false); 1082 | } 1083 | 1084 | /** 1085 | * @description Creates an New SObject (No Id) without issuing DML 1086 | * 1087 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1088 | **/ 1089 | public Account buildNew() { 1090 | return (Account)build(true); 1091 | } 1092 | 1093 | /** 1094 | * @description Persists builder and its related data through Unit Of Work 1095 | * 1096 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1097 | **/ 1098 | public Account persist(fflib_ISObjectUnitOfWork uow) { 1099 | return (Account)persistBuilder(uow); 1100 | } 1101 | 1102 | /** 1103 | * @description Registers instance for persistance via persistBuilders 1104 | * 1105 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1106 | **/ 1107 | public TestAccountBuilder register() { 1108 | return (TestAccountBuilder)registerBuilder(); 1109 | } 1110 | 1111 | /** 1112 | * @description Returns Contact SObject associated to this builder 1113 | * 1114 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1115 | **/ 1116 | public Account Record { 1117 | get { return (Account)getRecord(); } 1118 | private set; 1119 | } 1120 | 1121 | /** 1122 | * @description Returns a Clone of this instance 1123 | **/ 1124 | public TestAccountBuilder but() { 1125 | return new TestAccountBuilder(this); 1126 | } 1127 | 1128 | /** 1129 | * Methods/Properties above would be included in a basic template for any derived builder class 1130 | * 1131 | * END STANDARD BUILDER TEMPLATE 1132 | * ------------------------------- 1133 | **/ 1134 | 1135 | /** 1136 | * @description Remaining methods are SObject specific and support fluent configuration of field values 1137 | **/ 1138 | public TestAccountBuilder withName(String value) { 1139 | set(Account.Name, value); 1140 | return this; 1141 | } 1142 | 1143 | public TestAccountBuilder withType(String value) { 1144 | set(Account.Type, value); 1145 | return this; 1146 | } 1147 | } 1148 | 1149 | private class TestContactBuilder extends TestBuilderBase 1150 | { 1151 | /** 1152 | * Methods/Properties below would be included in a basic template for any derived builder class 1153 | * 1154 | * BEGIN STANDARD BUILDER TEMPLATE 1155 | * ------------------------------- 1156 | **/ 1157 | 1158 | /** 1159 | * @description Constructor should be included in every derived builder 1160 | **/ 1161 | private TestContactBuilder() { 1162 | super(Contact.SObjectType); 1163 | } 1164 | 1165 | /** 1166 | * @description Constructor should be included in every derived builder 1167 | **/ 1168 | private TestContactBuilder(TestContactBuilder copyFrom) { 1169 | super(copyFrom); 1170 | } 1171 | 1172 | /** 1173 | * @description Creates an existing SObject without issuing DML 1174 | * 1175 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1176 | **/ 1177 | public Contact build() { 1178 | return (Contact)build(false); 1179 | } 1180 | 1181 | /** 1182 | * @description Creates an New SObject (No Id) without issuing DML 1183 | * 1184 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1185 | **/ 1186 | public Contact buildNew() { 1187 | return (Contact)build(true); 1188 | } 1189 | 1190 | /** 1191 | * @description Persists builder and its related data through Unit Of Work 1192 | * 1193 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1194 | **/ 1195 | public Contact persist(fflib_ISObjectUnitOfWork uow) { 1196 | return (Contact)persistBuilder(uow); 1197 | } 1198 | 1199 | /** 1200 | * @description Registers instance for persistance via persistBuilders 1201 | * 1202 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1203 | **/ 1204 | public TestContactBuilder register() { 1205 | return (TestContactBuilder)registerBuilder(); 1206 | } 1207 | 1208 | /** 1209 | * @description Returns Contact SObject associated to this builder 1210 | * 1211 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1212 | **/ 1213 | public Contact Record { 1214 | get { return (Contact)getRecord(); } 1215 | private set; 1216 | } 1217 | 1218 | /** 1219 | * @description Returns a Clone of this instance 1220 | **/ 1221 | public TestContactBuilder but() { 1222 | return new TestContactBuilder(this); 1223 | } 1224 | 1225 | /** 1226 | * Methods/Properties above would be included in a basic template for any derived builder class 1227 | * 1228 | * END STANDARD BUILDER TEMPLATE 1229 | * ------------------------------- 1230 | **/ 1231 | 1232 | /** 1233 | * @description Remaining methods are SObject specific and support fluent configuration of field values 1234 | **/ 1235 | public TestContactBuilder withLastName(String value) { 1236 | set(Contact.LastName, value); 1237 | return this; 1238 | } 1239 | 1240 | public TestContactBuilder withFirstName(String value) { 1241 | set(Contact.FirstName, value); 1242 | return this; 1243 | } 1244 | 1245 | public TestContactBuilder withAccount(TestAccountBuilder value) { 1246 | setParent(Contact.AccountId, value); 1247 | return this; 1248 | } 1249 | } 1250 | 1251 | private class TestOpportunityBuilder extends TestBuilderBase 1252 | { 1253 | /** 1254 | * Methods/Properties below would be included in a basic template for any derived builder class 1255 | * 1256 | * BEGIN STANDARD BUILDER TEMPLATE 1257 | * ------------------------------- 1258 | **/ 1259 | 1260 | /** 1261 | * @description Constructor should be included in every derived builder 1262 | **/ 1263 | private TestOpportunityBuilder() { 1264 | super(Opportunity.SObjectType); 1265 | } 1266 | 1267 | /** 1268 | * @description Constructor should be included in every derived builder 1269 | **/ 1270 | private TestOpportunityBuilder(TestOpportunityBuilder copyFrom) { 1271 | super(copyFrom); 1272 | } 1273 | 1274 | /** 1275 | * @description Creates an existing SObject without issuing DML 1276 | * 1277 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1278 | **/ 1279 | public Opportunity build() { 1280 | return (Opportunity)build(false); 1281 | } 1282 | 1283 | /** 1284 | * @description Creates an New SObject (No Id) without issuing DML 1285 | * 1286 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1287 | **/ 1288 | public Opportunity buildNew() { 1289 | return (Opportunity)build(true); 1290 | } 1291 | 1292 | /** 1293 | * @description Persists builder and its related data through Unit Of Work 1294 | * 1295 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1296 | **/ 1297 | public Opportunity persist(fflib_ISObjectUnitOfWork uow) { 1298 | return (Opportunity)persistBuilder(uow); 1299 | } 1300 | 1301 | /** 1302 | * @description Registers instance for persistance via persistBuilders 1303 | * 1304 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1305 | **/ 1306 | public TestOpportunityBuilder register() { 1307 | return (TestOpportunityBuilder)registerBuilder(); 1308 | } 1309 | 1310 | /** 1311 | * @description Returns Contact SObject associated to this builder 1312 | * 1313 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1314 | **/ 1315 | public Opportunity Record { 1316 | get { return (Opportunity)getRecord(); } 1317 | private set; 1318 | } 1319 | 1320 | /** 1321 | * @description Returns a Clone of this instance 1322 | **/ 1323 | public TestOpportunityBuilder but() { 1324 | return new TestOpportunityBuilder(this); 1325 | } 1326 | 1327 | /** 1328 | * Methods/Properties above would be included in a basic template for any derived builder class 1329 | * 1330 | * END STANDARD BUILDER TEMPLATE 1331 | * ------------------------------- 1332 | **/ 1333 | 1334 | /** 1335 | * @description Remaining methods are SObject specific and support fluent configuration of field values 1336 | **/ 1337 | public TestOpportunityBuilder withAccount(TestAccountBuilder value) { 1338 | setParent(Opportunity.AccountId, value); 1339 | return this; 1340 | } 1341 | 1342 | public TestOpportunityBuilder withName(String value) { 1343 | set(Opportunity.Name, value); 1344 | return this; 1345 | } 1346 | 1347 | public TestOpportunityBuilder withAmount(Decimal value) { 1348 | set(Opportunity.Amount, value); 1349 | return this; 1350 | } 1351 | 1352 | public TestOpportunityBuilder withStageName(String value) { 1353 | set(Opportunity.StageName, value); 1354 | return this; 1355 | } 1356 | 1357 | public TestOpportunityBuilder withCloseDate(Date value) { 1358 | set(Opportunity.CloseDate, value); 1359 | return this; 1360 | } 1361 | 1362 | public TestOpportunityBuilder withType(String value) { 1363 | set(Opportunity.Type, value); 1364 | return this; 1365 | } 1366 | } 1367 | 1368 | private class TestOpportunityLineItemBuilder extends TestBuilderBase 1369 | { 1370 | /** 1371 | * Methods/Properties below would be included in a basic template for any derived builder class 1372 | * 1373 | * BEGIN STANDARD BUILDER TEMPLATE 1374 | * ------------------------------- 1375 | **/ 1376 | 1377 | /** 1378 | * @description Constructor should be included in every derived builder 1379 | **/ 1380 | private TestOpportunityLineItemBuilder() { 1381 | super(OpportunityLineItem.SObjectType); 1382 | } 1383 | 1384 | /** 1385 | * @description Constructor should be included in every derived builder 1386 | **/ 1387 | private TestOpportunityLineItemBuilder(TestOpportunityLineItemBuilder copyFrom) { 1388 | super(copyFrom); 1389 | } 1390 | 1391 | /** 1392 | * @description Creates an existing SObject without issuing DML 1393 | * 1394 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1395 | **/ 1396 | public OpportunityLineItem build() { 1397 | return (OpportunityLineItem)build(false); 1398 | } 1399 | 1400 | /** 1401 | * @description Creates an New SObject (No Id) without issuing DML 1402 | * 1403 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1404 | **/ 1405 | public OpportunityLineItem buildNew() { 1406 | return (OpportunityLineItem)build(true); 1407 | } 1408 | 1409 | /** 1410 | * @description Persists builder and its related data through Unit Of Work 1411 | * 1412 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1413 | **/ 1414 | public OpportunityLineItem persist(fflib_ISObjectUnitOfWork uow) { 1415 | return (OpportunityLineItem)persistBuilder(uow); 1416 | } 1417 | 1418 | /** 1419 | * @description Registers instance for persistance via persistBuilders 1420 | * 1421 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1422 | **/ 1423 | public TestOpportunityLineItemBuilder register() { 1424 | return (TestOpportunityLineItemBuilder)registerBuilder(); 1425 | } 1426 | 1427 | /** 1428 | * @description Returns Contact SObject associated to this builder 1429 | * 1430 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1431 | **/ 1432 | public OpportunityLineItem Record { 1433 | get { return (OpportunityLineItem)getRecord(); } 1434 | private set; 1435 | } 1436 | 1437 | /** 1438 | * @description Returns a Clone of this instance 1439 | **/ 1440 | public TestOpportunityLineItemBuilder but() { 1441 | return new TestOpportunityLineItemBuilder(this); 1442 | } 1443 | 1444 | /** 1445 | * Methods/Properties above would be included in a basic template for any derived builder class 1446 | * 1447 | * END STANDARD BUILDER TEMPLATE 1448 | * ------------------------------- 1449 | **/ 1450 | 1451 | /** 1452 | * @description Remaining methods are SObject specific and support fluent configuration of field values 1453 | **/ 1454 | public TestOpportunityLineItemBuilder withQuantity(Decimal value) { 1455 | set(OpportunityLineItem.Quantity, value); 1456 | return this; 1457 | } 1458 | 1459 | public TestOpportunityLineItemBuilder withTotalPrice(Decimal value) { 1460 | set(OpportunityLineItem.TotalPrice, value); 1461 | return this; 1462 | } 1463 | 1464 | public TestOpportunityLineItemBuilder withPriceBookEntry(TestPriceBookEntryBuilder value) { 1465 | setParent(OpportunityLineItem.PriceBookEntryId, value); 1466 | return this; 1467 | } 1468 | 1469 | public TestOpportunityLineItemBuilder withOpportunity(TestOpportunityBuilder value) { 1470 | setParent(OpportunityLineItem.OpportunityId, value); 1471 | return this; 1472 | } 1473 | } 1474 | 1475 | private class TestProductBuilder extends TestBuilderBase 1476 | { 1477 | /** 1478 | * Methods/Properties below would be included in a basic template for any derived builder class 1479 | * 1480 | * BEGIN STANDARD BUILDER TEMPLATE 1481 | * ------------------------------- 1482 | **/ 1483 | 1484 | /** 1485 | * @description Constructor should be included in every derived builder 1486 | **/ 1487 | private TestProductBuilder() { 1488 | super(Product2.SObjectType); 1489 | } 1490 | 1491 | /** 1492 | * @description Constructor should be included in every derived builder 1493 | **/ 1494 | private TestProductBuilder(TestProductBuilder copyFrom) { 1495 | super(copyFrom); 1496 | } 1497 | 1498 | /** 1499 | * @description Creates an existing SObject without issuing DML 1500 | * 1501 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1502 | **/ 1503 | public Product2 build() { 1504 | return (Product2)build(false); 1505 | } 1506 | 1507 | /** 1508 | * @description Creates an New SObject (No Id) without issuing DML 1509 | * 1510 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1511 | **/ 1512 | public Product2 buildNew() { 1513 | return (Product2)build(true); 1514 | } 1515 | 1516 | /** 1517 | * @description Persists builder and its related data through Unit Of Work 1518 | * 1519 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1520 | **/ 1521 | public Product2 persist(fflib_ISObjectUnitOfWork uow) { 1522 | return (Product2)persistBuilder(uow); 1523 | } 1524 | 1525 | /** 1526 | * @description Registers instance for persistance via persistBuilders 1527 | * 1528 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1529 | **/ 1530 | public TestProductBuilder register() { 1531 | return (TestProductBuilder)registerBuilder(); 1532 | } 1533 | 1534 | /** 1535 | * @description Returns Contact SObject associated to this builder 1536 | * 1537 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1538 | **/ 1539 | public Product2 Record { 1540 | get { return (Product2)getRecord(); } 1541 | private set; 1542 | } 1543 | 1544 | /** 1545 | * @description Returns a Clone of this instance 1546 | **/ 1547 | public TestProductBuilder but() { 1548 | return new TestProductBuilder(this); 1549 | } 1550 | 1551 | /** 1552 | * Methods/Properties above would be included in a basic template for any derived builder class 1553 | * 1554 | * END STANDARD BUILDER TEMPLATE 1555 | * ------------------------------- 1556 | **/ 1557 | 1558 | /** 1559 | * @description Remaining methods are SObject specific and support fluent configuration of field values 1560 | **/ 1561 | public TestProductBuilder withName(String value) { 1562 | set(Product2.Name, value); 1563 | return this; 1564 | } 1565 | } 1566 | 1567 | private class TestPriceBookBuilder extends TestBuilderBase 1568 | { 1569 | /** 1570 | * Methods/Properties below would be included in a basic template for any derived builder class 1571 | * 1572 | * BEGIN STANDARD BUILDER TEMPLATE 1573 | * ------------------------------- 1574 | **/ 1575 | 1576 | /** 1577 | * @description Constructor should be included in every derived builder 1578 | **/ 1579 | private TestPriceBookBuilder() { 1580 | super(Pricebook2.SObjectType); 1581 | } 1582 | 1583 | /** 1584 | * @description Constructor should be included in every derived builder 1585 | **/ 1586 | private TestPriceBookBuilder(TestPriceBookBuilder copyFrom) { 1587 | super(copyFrom); 1588 | } 1589 | 1590 | /** 1591 | * @description Creates an existing SObject without issuing DML 1592 | * 1593 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1594 | **/ 1595 | public Pricebook2 build() { 1596 | return (Pricebook2)build(false); 1597 | } 1598 | 1599 | /** 1600 | * @description Creates an New SObject (No Id) without issuing DML 1601 | * 1602 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1603 | **/ 1604 | public Pricebook2 buildNew() { 1605 | return (Pricebook2)build(true); 1606 | } 1607 | 1608 | /** 1609 | * @description Persists builder and its related data through Unit Of Work 1610 | * 1611 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1612 | **/ 1613 | public Pricebook2 persist(fflib_ISObjectUnitOfWork uow) { 1614 | return (Pricebook2)persistBuilder(uow); 1615 | } 1616 | 1617 | /** 1618 | * @description Registers instance for persistance via persistBuilders 1619 | * 1620 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1621 | **/ 1622 | public TestPriceBookBuilder register() { 1623 | return (TestPriceBookBuilder)registerBuilder(); 1624 | } 1625 | 1626 | /** 1627 | * @description Returns Contact SObject associated to this builder 1628 | * 1629 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1630 | **/ 1631 | public Pricebook2 Record { 1632 | get { return (Pricebook2)getRecord(); } 1633 | private set; 1634 | } 1635 | 1636 | /** 1637 | * @description Returns a Clone of this instance 1638 | **/ 1639 | public TestPriceBookBuilder but() { 1640 | return new TestPriceBookBuilder(this); 1641 | } 1642 | 1643 | /** 1644 | * Methods/Properties above would be included in a basic template for any derived builder class 1645 | * 1646 | * END STANDARD BUILDER TEMPLATE 1647 | * ------------------------------- 1648 | **/ 1649 | 1650 | /** 1651 | * @description Remaining methods are SObject specific and support fluent configuration of field values 1652 | **/ 1653 | public TestPriceBookBuilder withName(String value) { 1654 | set(Pricebook2.Name, value); 1655 | return this; 1656 | } 1657 | 1658 | public TestPriceBookBuilder withIsActive(Boolean value) { 1659 | set(Pricebook2.IsActive, value); 1660 | return this; 1661 | } 1662 | } 1663 | 1664 | private class TestPriceBookEntryBuilder extends TestBuilderBase 1665 | { 1666 | /** 1667 | * Methods/Properties below would be included in a basic template for any derived builder class 1668 | * 1669 | * BEGIN STANDARD BUILDER TEMPLATE 1670 | * ------------------------------- 1671 | **/ 1672 | 1673 | /** 1674 | * @description Constructor should be included in every derived builder 1675 | **/ 1676 | private TestPriceBookEntryBuilder() { 1677 | super(PricebookEntry.SObjectType); 1678 | } 1679 | 1680 | /** 1681 | * @description Constructor should be included in every derived builder 1682 | **/ 1683 | private TestPriceBookEntryBuilder(TestPriceBookEntryBuilder copyFrom) { 1684 | super(copyFrom); 1685 | } 1686 | 1687 | /** 1688 | * @description Creates an existing SObject without issuing DML 1689 | * 1690 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1691 | **/ 1692 | public PricebookEntry build() { 1693 | return (PricebookEntry)build(false); 1694 | } 1695 | 1696 | /** 1697 | * @description Creates an New SObject (No Id) without issuing DML 1698 | * 1699 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1700 | **/ 1701 | public PricebookEntry buildNew() { 1702 | return (PricebookEntry)build(true); 1703 | } 1704 | 1705 | /** 1706 | * @description Persists builder and its related data through Unit Of Work 1707 | * 1708 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1709 | **/ 1710 | public PricebookEntry persist(fflib_ISObjectUnitOfWork uow) { 1711 | return (PricebookEntry)persistBuilder(uow); 1712 | } 1713 | 1714 | /** 1715 | * @description Registers instance for persistance via persistBuilders 1716 | * 1717 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1718 | **/ 1719 | public TestPriceBookEntryBuilder register() { 1720 | return (TestPriceBookEntryBuilder)registerBuilder(); 1721 | } 1722 | 1723 | /** 1724 | * @description Returns Contact SObject associated to this builder 1725 | * 1726 | * @remarks Wrapper method to base class to allow for casting of specific SObjectType 1727 | **/ 1728 | public PricebookEntry Record { 1729 | get { return (PricebookEntry)getRecord(); } 1730 | private set; 1731 | } 1732 | 1733 | /** 1734 | * @description Returns a Clone of this instance 1735 | **/ 1736 | public TestPriceBookEntryBuilder but() { 1737 | return new TestPriceBookEntryBuilder(this); 1738 | } 1739 | 1740 | /** 1741 | * Methods/Properties above would be included in a basic template for any derived builder class 1742 | * 1743 | * END STANDARD BUILDER TEMPLATE 1744 | * ------------------------------- 1745 | **/ 1746 | 1747 | /** 1748 | * @description Remaining methods are SObject specific and support fluent configuration of field values 1749 | **/ 1750 | public TestPriceBookEntryBuilder withUnitPrice(Decimal value) { 1751 | set(PricebookEntry.UnitPrice, value); 1752 | return this; 1753 | } 1754 | 1755 | public TestPriceBookEntryBuilder withIsActive(Boolean value) { 1756 | set(PricebookEntry.IsActive, value); 1757 | return this; 1758 | } 1759 | 1760 | public TestPriceBookEntryBuilder withUseStandardPrice(Boolean value) { 1761 | set(PriceBookEntry.UseStandardPrice, value); 1762 | return this; 1763 | } 1764 | 1765 | public TestPriceBookEntryBuilder withStandardPriceBook() { 1766 | set(PriceBookEntry.PriceBook2Id, Test.getStandardPricebookId()); 1767 | return this; 1768 | } 1769 | 1770 | public TestPriceBookEntryBuilder withPriceBook(TestPriceBookBuilder value) { 1771 | setParent(PriceBookEntry.PriceBook2Id, value); 1772 | return this; 1773 | } 1774 | 1775 | public TestPriceBookEntryBuilder withProduct(TestProductBuilder value) { 1776 | setParent(PriceBookEntry.Product2Id, value); 1777 | return this; 1778 | } 1779 | } 1780 | 1781 | /** 1782 | * General exception class for test class 1783 | **/ 1784 | public class TestDomainBuilderException extends Exception {} 1785 | } -------------------------------------------------------------------------------- /sfdx-source/apex-common-builder/test/classes/fflib_DomainObjectBuilderTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 63.0 4 | Active 5 | 6 | --------------------------------------------------------------------------------