Skip to content

Commit

Permalink
Kitchen sink example
Browse files Browse the repository at this point in the history
  • Loading branch information
cesarParra committed Mar 28, 2022
1 parent 2af1478 commit 6b753f5
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 56 deletions.
135 changes: 107 additions & 28 deletions example/tests/IntegrationTests.cls
Original file line number Diff line number Diff line change
Expand Up @@ -188,32 +188,111 @@ private class IntegrationTests {
System.assertEquals(parentAccount.Id, childAccount.ParentId, 'Expected the relationship to have been correctly created.');
}

// TODO: This is getting out of control. Let's instead create our own data structure that is a bit simpler to get that
// to work first. Once we can get that to work then let's come back here and see if we can take care of this more
// complex structure. Let's use this as our kitchen sink example that everything can work, including even having
// defaulted relationships in custom TestDataBuilder implementations
// @IsTest
// private static void canCreateMultipleLevelsOfChildren() {
// Account anyAccount = new Account(Name = 'Test');
// insert anyAccount;
// Pricebook2 book = new Pricebook2();
// insert
// PricebookEntry entry = new PricebookEntry(UnitPrice = 100);
// insert entry;
//
// ITestDataBuilder orderItemRecord = SObjectTestDataBuilder.of(OrderItem.SObjectType)
// .with(OrderItem.PricebookEntryId, entry.Id)
// .with(OrderItem.UnitPrice, 100);
// ITestDataBuilder productBuilder = SObjectTestDataBuilder.of(Product2.SObjectType)
// .withChild(orderItemRecord, OrderItem.Product2Id); // TODO: A way to now be able to get the generated record out of this
// Order orderRecord = (Order)SObjectTestDataBuilder.of(Order.SObjectType)
// .with(Order.AccountId, anyAccount.Id) // TODO: See how we can make this dynamic
// .with(Order.EffectiveDate, Date.today()) // TODO: Why did this not default? Answer: Because I commented out callbacks
// .with(Order.Status, 'Draft') // TODO: Why did this not default? Answer: Because I commented out. But still we want to be better about defaulting restricted picklists
// .withChild(orderItemRecord, OrderItem.OrderId)
// .insertNew();
// SObjectTestDataBuilder.commitRecords();
// }

// TODO: What happens when calling commit multiple times
@IsTest
private static void kitchenSinkExample() {
// Entity Relationships:
// Order => Parents: Account, Pricebook2
// OrderItem => Parents: Order and

SObjectTestDataBuilder.registerBuilder(PricebookEntry.SObjectType, IntegrationTests.PriceBookEntryBuilder.class);
SObjectTestDataBuilder.registerBuilder(Order.SObjectType, IntegrationTests.OrderBuilder.class);
SObjectTestDataBuilder.registerBuilder(OrderItem.SObjectType, IntegrationTests.OrderItemBuilder.class);

ITestDataBuilder orderItemRecord = SObjectTestDataBuilder.of(OrderItem.SObjectType)
.with(OrderItem.UnitPrice, 100.00)
.with(OrderItem.Quantity, 1);
Order orderRecord = (Order)SObjectTestDataBuilder.of(Order.SObjectType)
.withChild(orderItemRecord, OrderItem.OrderId)
.registerNewForInsert();
SObjectTestDataBuilder.commitRecords();

// Assert that the relationships were correctly created
System.assertNotEquals(null, orderRecord.Id);
OrderItem itemRecord = (OrderItem)SObjectTestDataBuilder.getChildrenOfTypeById(orderRecord.Id, OrderItem.SObjectType)[0];
System.assertNotEquals(null, itemRecord.Id);
System.assertEquals(orderRecord.Id, itemRecord.OrderId);

// Assert that the correct records were inserted to the database
List<Order> dbOrders = [SELECT Id, (SELECT Id FROM OrderItems) FROM Order];
System.assertEquals(1, dbOrders.size());
System.assertEquals(1, dbOrders[0].OrderItems.size());
}

public class PriceBookEntryBuilder extends SObjectTestDataBuilder implements ITestDataBuilder {
public override SObjectType getSObjectType() {
return PricebookEntry.SObjectType;
}

public PriceBookEntryBuilder with(SObjectField field, Object value) {
return (PriceBookEntryBuilder)withData(field, value);
}

public PricebookEntry registerNewForInsert() {
return (PricebookEntry) this.registerSObjectForInsert();
}

public List<PricebookEntry> registerNewForInsert(Integer numberOfRecords) {
return this.registerSObjectsForInsert(numberOfRecords);
}

protected override Map<SObjectField, Object> getDefaultValueMap() {
return new Map<SObjectField, Object> {
PricebookEntry.Product2Id => bindTo(SObjectTestDataBuilder.of(Product2.SObjectType)),
PricebookEntry.Pricebook2Id => Test.getStandardPricebookId(),
PricebookEntry.UnitPrice => 100
};
}
}

public class OrderBuilder extends SObjectTestDataBuilder implements ITestDataBuilder {
public override SObjectType getSObjectType() {
return Order.SObjectType;
}

public OrderBuilder with(SObjectField field, Object value) {
return (OrderBuilder)withData(field, value);
}

public Order registerNewForInsert() {
return (Order) this.registerSObjectForInsert();
}

public List<Order> registerNewForInsert(Integer numberOfRecords) {
return this.registerSObjectsForInsert(numberOfRecords);
}

protected override Map<SObjectField, Object> getDefaultValueMap() {
return new Map<SObjectField, Object> {
Order.Status => 'Draft',
Order.AccountId => bindTo(SObjectTestDataBuilder.of(Account.SObjectType)),
Order.EffectiveDate => Date.today(),
Order.Pricebook2Id => Test.getStandardPricebookId()
};
}
}

public class OrderItemBuilder extends SObjectTestDataBuilder implements ITestDataBuilder {
public override SObjectType getSObjectType() {
return OrderItem.SObjectType;
}

public OrderItemBuilder with(SObjectField field, Object value) {
return (OrderItemBuilder)withData(field, value);
}

public OrderItem registerNewForInsert() {
return (OrderItem) this.registerSObjectForInsert();
}

public List<OrderItem> registerNewForInsert(Integer numberOfRecords) {
return this.registerSObjectsForInsert(numberOfRecords);
}

protected override Map<SObjectField, Object> getDefaultValueMap() {
return new Map<SObjectField, Object> {
OrderItem.PricebookEntryId => bindTo(SObjectTestDataBuilder.of(PricebookEntry.SObjectType)),
OrderItem.OrderId => bindTo(SObjectTestDataBuilder.of(Order.SObjectType))
};
}
}
}
112 changes: 84 additions & 28 deletions force-app/main/default/classes/SObjectTestDataBuilder.cls
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ public abstract class SObjectTestDataBuilder implements ITestDataBuilder, TestDa
return builder;
}

public static void registerBuilder(SObjectType objectType , Type builderType) {
builderCache.register(objectType, builderType);
}

public static SObject getAny(SObjectType objectType) {
SObject previouslyInserted = testDataHistory.getA(objectType);
if (previouslyInserted != null) {
Expand All @@ -42,9 +46,9 @@ public abstract class SObjectTestDataBuilder implements ITestDataBuilder, TestDa

public static void commitRecords() {
List<LateBinding> allLateBindings = new List<LateBinding>();
// Using this technique because throughout this process the allBindings list could be modified
// if more parents need to be added up the chain when registerNewForInsert gets called.
while (allBindings.size() > 0) {
// Using this technique because throughout this process the allBindings list could be modified
// if more parents need to be added up the chain when registerNewForInsert gets called.
LateBinding binding = allBindings.remove(0);
allLateBindings.add(binding);
SObject parentRecord = binding.builder.registerNewForInsert();
Expand Down Expand Up @@ -96,15 +100,6 @@ public abstract class SObjectTestDataBuilder implements ITestDataBuilder, TestDa

public abstract SObjectType getSObjectType();

public List<SObject> createNew(Integer numberOfRecords) {
List<SObject> recordsToInsert = new List<SObject>();
for (Integer i = 0; i < numberOfRecords; i++) {
SObject recordToInsert = build();
recordsToInsert.add(recordToInsert);
}
return recordsToInsert;
}

public SObjectTestDataBuilder withChild(ITestDataBuilder childBuilder, SObjectField relationshipField) {
this.registeredRelationships.add(new ChildRelationship(childBuilder, relationshipField));
return this;
Expand All @@ -122,6 +117,10 @@ public abstract class SObjectTestDataBuilder implements ITestDataBuilder, TestDa
return this;
}

protected virtual Map<SObjectField, Object> getDefaultValueMap() {
return new Map<SObjectField, Object>();
}

protected SObject registerSObjectForInsert() {
return this.registerSObjectsForInsert(1)[0];
}
Expand All @@ -131,15 +130,67 @@ public abstract class SObjectTestDataBuilder implements ITestDataBuilder, TestDa
}

protected List<SObject> registerSObjects(Integer numberOfRecordsToInsert, SObjectField parentRelationship, SObject relatedTo) {
List<SObject> recordsToInsert = createNew(numberOfRecordsToInsert);
testDataHistory.log(recordsToInsert, relatedTo, parentRelationship, this);
registerChildren(recordsToInsert);
List<SObject> recordsToRegister = createNew(numberOfRecordsToInsert);
testDataHistory.log(recordsToRegister, relatedTo, parentRelationship, this);
registerChildren(recordsToRegister);
lateBind(recordsToRegister);
clear();
return recordsToInsert;
return recordsToRegister;
}

protected virtual Map<SObjectField, Object> getDefaultValueMap() {
return new Map<SObjectField, Object>();
private void lateBind(List<SObject> registeredRecords) {
for (SObject currentRecord : registeredRecords) {
for (SObjectField defaultField : defaultValueMapCached.keySet()) {
if (!customValueMap.containsKey(defaultField)) {
// Skip any field that has been overridden through the custom value map
Object fieldValue = defaultValueMapCached.get(defaultField);
if (checkIfBinding(fieldValue)) {
attemptAddBinding((LateBinding)fieldValue, currentRecord, defaultField, false);
}
}
}

for (SObjectField customField : customValueMap.keySet()) {
if (customValueMap.get(customField) != null) {
Object fieldValue = customValueMap.get(customField);
if (checkIfBinding(fieldValue)) {
attemptAddBinding((LateBinding)fieldValue, currentRecord, customField, true);
}
}
}
}
}

private void attemptAddBinding(LateBinding binding, SObject record, SObjectField relationshipField, Boolean throwErrorIfRegistered) {
for (HistoryItem item : testDataHistory.getAllNotInserted()) {
// If the record is already logged with a parent relationship to the same field, then we do not
// want to bind it.
// This can happen if the record was logged as part of a withChild call for its parent
if (item.getRecord() == record && item.getRelationshipField() == relationshipField) {
if (throwErrorIfRegistered) {
String errorMessage = 'The record {0} is already registered to a parent through the field {1}. ' +
'make sure to avoid registering a parent to a record that is being created through withChild for the same field';
throw new SObjectTestDataBuilderException(String.format(errorMessage, new List<String> {
String.valueOf(record),
relationshipField.getDescribe().getName()
}));
}
return;
}
}

binding.setRelationshipField(relationshipField);
binding.setChildRecord(record);
allBindings.add(binding);
}

private List<SObject> createNew(Integer numberOfRecords) {
List<SObject> recordsToInsert = new List<SObject>();
for (Integer i = 0; i < numberOfRecords; i++) {
SObject recordToInsert = build();
recordsToInsert.add(recordToInsert);
}
return recordsToInsert;
}

private SObject build() {
Expand All @@ -149,7 +200,7 @@ public abstract class SObjectTestDataBuilder implements ITestDataBuilder, TestDa
if (!customValueMap.containsKey(defaultField)) {
// Skip any field that has been overridden through the custom value map
Object fieldValue = defaultValueMapCached.get(defaultField);
if (checkIfBinding(fieldValue, instance, defaultField)) {
if (checkIfBinding(fieldValue)) {
continue;
}
instance.put(defaultField, fieldValue);
Expand All @@ -159,7 +210,7 @@ public abstract class SObjectTestDataBuilder implements ITestDataBuilder, TestDa
for (SObjectField customField : customValueMap.keySet()) {
if (customValueMap.get(customField) != null) {
Object fieldValue = customValueMap.get(customField);
if (checkIfBinding(fieldValue, instance, customField)) {
if (checkIfBinding(fieldValue)) {
continue;
}
instance.put(customField, fieldValue);
Expand All @@ -169,22 +220,18 @@ public abstract class SObjectTestDataBuilder implements ITestDataBuilder, TestDa
return instance;
}

private Boolean checkIfBinding(Object fieldValue, SObject record, SObjectField relationshipField) {
private Boolean checkIfBinding(Object fieldValue) {
if (fieldValue instanceof LateBinding) {
LateBinding binding = (LateBinding)fieldValue;
binding.setRelationshipField(relationshipField);
binding.setChildRecord(record);
allBindings.add(binding);
return true;
}
return false;
}

private void registerChildren(List<SObject> insertedRecords) {
for (SObject insertedRecord : insertedRecords) {
private void registerChildren(List<SObject> records) {
for (SObject currentRecord : records) {
for (ChildRelationship relationship : this.registeredRelationships) {
SObjectTestDataBuilder relationshipAsTestData = (SObjectTestDataBuilder)relationship.ChildBuilder;
relationshipAsTestData.registerSObjects(1, relationship.RelationshipField, insertedRecord);
relationshipAsTestData.registerSObjects(1, relationship.RelationshipField, currentRecord);
}
}
}
Expand All @@ -203,6 +250,14 @@ public abstract class SObjectTestDataBuilder implements ITestDataBuilder, TestDa
this.builderTypeBySObjectType = new Map<SObjectType, Type>();
}

public void register(SObjectType objectType, Type classType) {
if (builderTypeBySObjectType.keySet().contains(objectType)) {
// If already registered, skip.
return;
}
this.builderTypeBySObjectType.put(objectType, classType);
}

public ITestDataBuilder getFor(SObjectType objectType) {
if (this.initialized == false) {
initializeCache();
Expand All @@ -223,7 +278,7 @@ public abstract class SObjectTestDataBuilder implements ITestDataBuilder, TestDa
Object currentClassInstance = currentClassType.newInstance();
if (currentClassInstance instanceof ITestDataBuilder && currentClassInstance instanceof SObjectTestDataBuilder) {
ITestDataBuilder builderInstance = (ITestDataBuilder)currentClassInstance;
this.builderTypeBySObjectType.put(builderInstance.getSObjectType(), currentClassType);
this.register(builderInstance.getSObjectType(), currentClassType);
}
} catch (Exception e) {
// An error might occur when creating the type. If it does we move on and don't add it to the cache.
Expand Down Expand Up @@ -349,6 +404,7 @@ public abstract class SObjectTestDataBuilder implements ITestDataBuilder, TestDa
if (historiesByType == null) {
historiesByType = new List<HistoryItem>();
}

historiesByType.add(item);
recordsByType.put(item.getRecord().getSObjectType(), historiesByType);
}
Expand Down

0 comments on commit 6b753f5

Please sign in to comment.