Skip to content

Commit

Permalink
Documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
cesarParra committed Apr 8, 2022
1 parent 6644da7 commit 02d0a3f
Show file tree
Hide file tree
Showing 6 changed files with 381 additions and 23 deletions.
199 changes: 188 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,195 @@
# Salesforce DX Project: Next Steps
<div align="center">
<h1>Salesforce Test Data Framework</h1>
<p>
Easy and intuitive way to create Salesforce unit test data.
</p>

Now that you’ve created a Salesforce DX project, what’s next? Here are some documentation resources to get you started.
<h4>
<a href="https://cesarparra.github.io/test-data-framework">Documentation</a>
<span> · </span>
<a href="https://github.com/cesarParra/test-data-framework/issues">Report Bug</a>
<span> · </span>
<a href="https://github.com/cesarParra/test-data-framework/issues">Request Feature</a>
</h4>
</div>

## How Do You Plan to Deploy Your Changes?
<br />

Do you want to deploy a set of changes, or create a self-contained application? Choose a [development model](https://developer.salesforce.com/tools/vscode/en/user-guide/development-models).
## About

## Configure Your Salesforce DX Project
The Salesforce Test Data Framework allows you to intuitively create Salesforce SObject data for your
unit tests using a fluent language.

The `sfdx-project.json` file contains useful configuration information for your project. See [Salesforce DX Project Configuration](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_ws_config.htm) in the _Salesforce DX Developer Guide_ for details about this file.
It provides base support for any SObject, standard or custom, out of the box, but allows you
to adapt it to your custom needs.

## Read All About It
## Getting Started

- [Salesforce Extensions Documentation](https://developer.salesforce.com/tools/vscode/)
- [Salesforce CLI Setup Guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_setup_intro.htm)
- [Salesforce DX Developer Guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_intro.htm)
- [Salesforce CLI Command Reference](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference.htm)
### Deploying

To deploy clone or download this repo and push the contents of `force-app/main/default/classes` to your org.

### Usage

#### Quick Start

Tip: See the `IntegrationTests` class for more in depth example usages of the framework.

A record can be created through the following syntax

```apex
Contact anyContact = (Contact)SObjectTestDataBuilder.of(Contact.SObjectType).registerNewForInsert();
SObjectTestDataBuilder.commitRecords();
```

`SObjectTestDataBuilder.of` receives an `SObjectType` for the type of record that you want to create. Calling
`registerNewForInsert` on the returned object will queue up the record for bulk insertion once
`SObjectTestDataBuilder.commitRecords()` gets called.

You can also create multiple records by using the `registerNewForInsert` overload that takes the number
of records to create

```apex
List<Contact> multipleContacts = (List<Contact>)SObjectTestDataBuilder.of(Contact.SObjectType).registerNewForInsert(5);
SObjectTestDataBuilder.commitRecords();
```

By default, the framework will try to resolve for any required fields on the SObject. In the `Contact` example,
the `LastName` field is a required text field, so the framework will use a `String` from the `DefaultTestData`
class to fill up that field.

Feel free to modify that class to change the defaults for any of the different field types.

During record creation you can also explicitly specify any field data you want to create by chaining `with` calls
and passing an SObjectField and a value:

```apex
Contact anyContact = (Contact)SObjectTestDataBuilder.of(Contact.SObjectType)
.with(Contact.FirstName, 'John')
.with(Contact.LastName, 'Smith')
.registerNewForInsert();
SObjectTestDataBuilder.commitRecords();
```

#### Relationships

When creating test data you might want to also create relationship records, either parent-to-child
or child-to-parent, which are both supported by the framework.

To create a **parent-to-child** relationship you can use the `withChild` method:

```apex
Account accountWithChild = (Account)SObjectTestDataBuilder.of(Account.SObjectType)
.withChild(
SObjectTestDataBuilder.of(Contact.SObjectType),
Contact.AccountId
)
.registerNewForInsert();
SObjectTestDataBuilder.commitRecords();
```

The `withChild` method takes 2 arguments: a builder for the child (which can be obtained by calling `SObjectTestDataBuilder.of`)
and the relationship field between the 2 objects.

**Retrieving child records**

Once the records have been inserted to the database (after the call to `SObjectTestDataBuilder.commitRecords()`) you do not
need to query the database to get the reference to the children. Instead, the framework keeps a reference of all inserted records
so you can retrieve the children by using the `SObjectTestDataBuilder.getChildrenOfTypeById` method:

```apex
List<Contact> accountChildren = SObjectTestDataBuilder.getChildrenOfTypeById(accountWithChild.Id, Contact.SObjectType);
```

To create **child-to-parent** relationships the framework has the concept of "late binding", which is a way to specify
a relationship between objects without calling the database until necessary for bulkification purposes.

The late binding is used with the regular `with` method that is used to specify any field value. But instead
of passing a record ID value (which *is* valid as well, but will not take advantage of bulkification) you pass
in a `SObjectTestDataBuilder.LateBinding` object, which can be obtained by calling the `SObjectTestDataBuilder.lateBinding`
method:

```apex
SObjectTestDataBuilder.of(OrderItem__c.SObjectType)
.with(OrderItem__c.Order__c, SObjectTestDataBuilder.lateBinding(Order__c.SObjectType))
.registerNewForInsert();
SObjectTestDataBuilder.commitRecords();
```

This will create both the `OrderItem__c` and the parent `Order__c`.

#### Custom Builders

When creating SObjects you will probably want more control over the data than what the default builder gives you. The framework
also allows you to create custom builders for different SObjectTypes.

To create a custom builder you will need to **both** extend `SObjectTestDataBuilder` and implement `ITestDataBuilder`.
For the framework to automatically detect your custom builder it also **needs* to have a name that ends in `TestDataBuilder`
(though there is a way to explicitly specify the builder to the framework, explained below):

```apex
@IsTest
public with sharing class OrderTestDataBuilder extends SObjectTestDataBuilder implements ITestDataBuilder {
public override SObjectType getSObjectType() {
return Order__c.SObjectType;
}
public OrderTestDataBuilder with(SObjectField field, Object value) {
return (OrderTestDataBuilder) withData(field, value);
}
public Order__c registerNewForInsert() {
return (Order__c) this.registerSObjectForInsert();
}
public List<SObject> registerNewForInsert(Integer numberOfRecords) {
return this.registerSObjectsForInsert(numberOfRecords);
}
protected override Map<SObjectField, Object> getDefaultValueMap() {
return new Map<SObjectField, Object> {
Order__c.Customer__c => bindTo(SObjectTestDataBuilder.of(Account.SObjectType))
};
}
}
```

Creating a custom builder gives you the following functionality:
* Allows you to create additional `withSomeField` method, which will make your code more fluent
* You can optionally override the `getDefaultValueMap` method, which will allow you to create default data tailored to the SObject.
* Note by the example that you can use late binding by calling the `bindTo` method automatically provided to you when extending `SObjectTestDataBuilder`.
* You can optionally override the `beforeInsert` and `afterInsert` methods, which will allow you to execute code before and after hitting the database for the insertion of your records.

#### Default resolution vs. Explicitly specifying

By default, the framework will look for any class that ends with `TestDataBuilder` to try and automatically resolve with test data builder to use.
When one is not found then the `DefaultTestDataBuilder` will be used, which will attempt to resolve required fields with default data. But you can also
explicitly specify which builder to use to the framework in the unit test itself, in case you want to either override the automatic resolution and use a different builder,
or if you simply don't want to follow the naming convention.

You can do that by using the `SObjectTestDataBuilder.registerBuilder` method:

```apex
SObjectTestDataBuilder.registerBuilder(Order.SObjectType, CustomOrderBuilder.class);
```


### Tests

This repo provides the `IntegrationTests` class with examples of all functionality provided by the framework. The
`kitchenSinkExample` provides an example of a complex hierarchy of objects.

## Roadmap

* [ ] Release as unmanaged package
* [ ] In-memory support


## Contributing

Contributions are always welcome!


## License

Distributed under the MIT License. See LICENSE file for more information.
11 changes: 0 additions & 11 deletions TODO.md

This file was deleted.

24 changes: 24 additions & 0 deletions docs/Misc/ITestDataCallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# ITestDataCallback

Callback with methods to be executed before and after database calls. Do not implement directly, rather extend [SObjectTestDataBuilder](/Misc/SObjectTestDataBuilder.md) and override the methods provided there.

## Methods
### `beforeInsert(SObject record)`

Executes before inserting the SObject for this builder into the database.

#### Parameters
|Param|Description|
|---|---|
|`record`|The SObject that will be inserted into the database.|

### `afterInsert(SObject record)`

Executes after inserting the SObject for this builder into the database.

#### Parameters
|Param|Description|
|---|---|
|`record`|The SObject that will be inserted into the database.|

---
120 changes: 120 additions & 0 deletions docs/Misc/TestDataUnitOfWork.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# TestDataUnitOfWork

`ISTEST`

Provides an implementation of the Enterprise Application Architecture Unit Of Work, as defined by Martin Fowler http://martinfowler.com/eaaCatalog/unitOfWork.html &quot;When you&apos;re pulling data in and out of a database, it&apos;s important to keep track of what you&apos;ve changed; otherwise, that data won&apos;t be written back into the database. Similarly you have to insert new objects you create and remove any objects you delete.&quot; &quot;You can change the database with each change to your object model, but this can lead to lots of very small database calls, which ends up being very slow. Furthermore it requires you to have a transaction open for the whole interaction, which is impractical if you have a business transaction that spans multiple requests. The situation is even worse if you need to keep track of the objects you&apos;ve read so you can avoid inconsistent reads.&quot; &quot;A Unit of Work keeps track of everything you do during a business transaction that can affect the database. When you&apos;re done, it figures out everything that needs to be done to alter the database as a result of your work.&quot; In an Apex context this pattern provides the following specific benefits - Applies bulkfication to DML operations, insert, update and delete - Manages a business transaction around the work and ensures a rollback occurs (even when exceptions are later handled by the caller) - Honours dependency rules between records and updates dependent relationships automatically during the commit

## Constructors
### `TestDataUnitOfWork(List<Schema.SObjectType> sObjectTypes)`

Constructs a new UnitOfWork to support work against the given object list

#### Parameters
|Param|Description|
|---|---|
|`sObjectTypes`|A list of objects given in dependency order (least dependent first)|

### `TestDataUnitOfWork(List<Schema.SObjectType> sObjectTypes, UnresolvedRelationshipBehavior unresolvedRelationshipBehavior)`

Constructs a new UnitOfWork to support work against the given object list

#### Parameters
|Param|Description|
|---|---|
|`sObjectTypes`|A list of objects given in dependency order (least dependent first)|
|`unresolvedRelationshipBehavior`|If AttemptOutOfOrderRelationships and a relationship is registered where a parent is inserted after a child then will update the child post-insert to set the relationship. If IgnoreOutOfOrder then relationship will not be set.|

### `TestDataUnitOfWork(List<Schema.SObjectType> sObjectTypes, IDML dml)`

Constructs a new UnitOfWork to support work against the given object list

#### Parameters
|Param|Description|
|---|---|
|`sObjectTypes`|A list of objects given in dependency order (least dependent first)|
|`dml`|Modify the database via this class|

### `TestDataUnitOfWork(List<Schema.SObjectType> sObjectTypes, IDML dml, UnresolvedRelationshipBehavior unresolvedRelationshipBehavior)`

Constructs a new UnitOfWork to support work against the given object list

#### Parameters
|Param|Description|
|---|---|
|`sObjectTypes`|A list of objects given in dependency order (least dependent first)|
|`dml`|Modify the database via this class|
|`unresolvedRelationshipBehavior`|If AttemptOutOfOrderRelationships and a relationship is registered where a parent is inserted after a child then will update the child post-insert to set the relationship. If IgnoreOutOfOrder then relationship will not be set.|

---
## Methods
### `onCommitWorkStarting()`
### `onCommitWorkFinishing()`
### `registerNew(SObject record, ITestDataCallback.ITestDataCallback callback)`

Register a newly created SObject instance to be inserted when commitWork is called

#### Parameters
|Param|Description|
|---|---|
|`record`|A newly created SObject instance to be inserted during commitWork|
|`callback`|TestDataCallback.ITestDataCallback to be called before and after record insert.|

### `registerRelationship(SObject record, Schema.SObjectField relatedToField, SObject relatedTo)`

Register a relationship between two records that have yet to be inserted to the database. This information will be used during the commitWork phase to make the references only when related records have been inserted to the database.

#### Parameters
|Param|Description|
|---|---|
|`record`|An existing or newly created record|
|`relatedToField`|A SObjectField reference to the lookup field that relates the two records together|
|`relatedTo`|A SObject instance (yet to be committed to the database)|

### `commitWork()`

Takes all the work that has been registered with the UnitOfWork and commits it to the database

---
## Enums
### UnresolvedRelationshipBehavior

Unit of work has two ways of resolving registered relationships that require an update to resolve (e.g. parent and child are same sobject type, or the parent is inserted after the child): AttemptResolveOutOfOrder - Update child to set the relationship (e.g. insert parent, insert child, update child) IgnoreOutOfOrder (default behaviour) - Do not set the relationship (e.g. leave lookup null)


---
## Classes
### SimpleDML
#### Methods
##### `dmlInsert(List&lt;SObject&gt; objList)`
###### Parameters
|Param|Description|
|---|---|

##### `dmlUpdate(List&lt;SObject&gt; objList)`
###### Parameters
|Param|Description|
|---|---|

---

### UnitOfWorkException

UnitOfWork Exception


---
## Interfaces
### IDML
#### Methods
##### `dmlInsert(List&lt;SObject&gt; objList)`
###### Parameters
|Param|Description|
|---|---|

##### `dmlUpdate(List&lt;SObject&gt; objList)`
###### Parameters
|Param|Description|
|---|---|

---

2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ Provides base functionality for building SObject records for testing purposes. A

### [TestDataUnitOfWork](/Misc/TestDataUnitOfWork.md)


Provides an implementation of the Enterprise Application Architecture Unit Of Work, as defined by Martin Fowler http://martinfowler.com/eaaCatalog/unitOfWork.html &quot;When you&apos;re pulling data in and out of a database, it&apos;s important to keep track of what you&apos;ve changed; otherwise, that data won&apos;t be written back into the database. Similarly you have to insert new objects you create and remove any objects you delete.&quot; &quot;You can change the database with each change to your object model, but this can lead to lots of very small database calls, which ends up being very slow. Furthermore it requires you to have a transaction open for the whole interaction, which is impractical if you have a business transaction that spans multiple requests. The situation is even worse if you need to keep track of the objects you&apos;ve read so you can avoid inconsistent reads.&quot; &quot;A Unit of Work keeps track of everything you do during a business transaction that can affect the database. When you&apos;re done, it figures out everything that needs to be done to alter the database as a result of your work.&quot; In an Apex context this pattern provides the following specific benefits - Applies bulkfication to DML operations, insert, update and delete - Manages a business transaction around the work and ensures a rollback occurs (even when exceptions are later handled by the caller) - Honours dependency rules between records and updates dependent relationships automatically during the commit
Loading

0 comments on commit 02d0a3f

Please sign in to comment.