Skip to content

A way to simplify your Salesforce data creation and method stubbing.

License

Notifications You must be signed in to change notification settings

apexfarm/ApexTestKit

Repository files navigation

Apex Test Kit

Apex Test Kit can help generate massive data for Apex test classes, including mock sObjects with read-only fields. It solves two pain points during data creation:

  1. Establish arbitrary levels of many-to-one, one-to-many relationships.
  2. Generate field values based on simple rules automatically.

It can also help generate method stubs with the help of Apex StubProvider interface underneath.

  1. Stubs are defined and verified with BDD given-when-then styles.
  2. Strict mode is enforced by default to help developers write clean mocking codes and increase productivity.
Environment Installation Link Version
Production, Developer ver 4.1
Sandbox ver 4.1

v4.x Release Notes

Major Changes

  • v4.0 Feature: Ported Mockito BDD Features
  • v4.1 Enhancement: ATK.SaveResult now supports getting list of Ids.

Next Steps

  • Performance tuning for the BDD features.
  • Enhance the BDD features.
  • Support HttpCalloutMock in BDD style.

🔥 Apex Test Kit with BDD

Please check the developer guide at this wiki page.

YourClass mock = (YourClass) ATK.mock(YourClass.class);
// Given
ATK.startStubbing();
ATK.given(mock.doSomething()).willReturn('Sth.');
ATK.stopStubbing();

// When
String returnValue = mock.doSomething();

// Then
System.assertEquals('Sth.', returnValue);
((ATKMockTest) ATK.then(mock).should().once()).doSomething();

Table of Contents

Introduction

Sales Object Graph

Imagine the complexity to generate all sObjects and relationships in the above diagram. With ATK we can create them within just one Apex statement. Here, we are generating:

  1. 200 accounts with names: Name-0001, Name-0002, Name-0003...
  2. Each of the accounts has 2 contacts.
  3. Each of the contacts has 1 opportunity via the OpportunityContactRole.
  4. Also each of the accounts has 2 orders.
  5. Also each of the orders belongs to 1 opportunity from the same account.
ATK.SaveResult result = ATK.prepare(Account.SObjectType, 200)
    .field(Account.Name).index('Name-{0000}')
    .withChildren(Contact.SObjectType, Contact.AccountId, 400)
        .field(Contact.LastName).index('Name-{0000}')
        .field(Contact.Email).index('test.user+{0000}@email.com')
        .field(Contact.MobilePhone).index('+86 186 7777 {0000}')
        .withChildren(OpportunityContactRole.SObjectType, OpportunityContactRole.ContactId, 400)
            .field(OpportunityContactRole.Role).repeat('Business User', 'Decision Maker')
            .withParents(Opportunity.SObjectType, OpportunityContactRole.OpportunityId, 400)
                .field(Opportunity.Name).index('Name-{0000}')
                .field(Opportunity.ForecastCategoryName).repeat('Pipeline')
                .field(Opportunity.Probability).repeat(0.9, 0.8)
                .field(Opportunity.StageName).repeat('Prospecting')
                .field(Opportunity.CloseDate).addDays(Date.newInstance(2020, 1, 1), 1)
                .field(Opportunity.TotalOpportunityQuantity).add(1000, 10)
                .withParents(Account.SObjectType, Opportunity.AccountId)
    .also(4)
    .withChildren(Order.SObjectType, Order.AccountId, 400)
        .field(Order.Name).index('Name-{0000}')
        .field(Order.EffectiveDate).addDays(Date.newInstance(2020, 1, 1), 1)
        .field(Order.Status).repeat('Draft')
        .withParents(Contact.SObjectType, Order.BillToContactId)
        .also()
        .withParents(Opportunity.SObjectType, Order.OpportunityId)
    .save();

Note: withChildren() and withParents() without a third size parameter indicate they will back reference the sObjects created previously in the statement.

Performance

The scripts used to perform benchmark testing are documented under scripts/apex/benchmark.apex. The results are averages of three successive insertions of 1000 accounts under the following conditions:

  1. No duplicate rules, process builders, and triggers etc.
  2. Debug level is set to DEBUG.
1000 * Account Database.insert ATK Save ATK Mock ATK Mock Perf.
CPU Time 2005 2304 1103 ~2x faster
Real Time (ms) 5672 5584 869 ~6.5x faster

Demos

Here are demos under the scripts/apex folder, they have been successfully run in fresh Salesforce organization with appropriate feature enabled. If not, please try to fix them with FLS, validation rules or duplicate rules etc.

Subject File Path Description
Campaign scripts/apex/demo-campaign.apex How to genereate campaigns with hierarchy relationships. ATK.EntityBuilder is implemented to reuse the field population logic.
Consumer Goods Cloud scripts/apex/demo-consumer.apex Create meaningful sObject relationship distributions in one ATK statement.
Sales scripts/apex/demo-sales.apex You've already seen it in the above paragraph.
Products scripts/apex/demo-products.apex How to generate PriceBook2, PriceBookEntry, Product2, ProductCategory, Catalog.
Cases scripts/apex/demo-cases.apex How to generate Accounts, Contacts and Cases.
Users scripts/apex/demo-users.apex How to generate community users in one goal.

Relationship

The object relationships described in a single ATK statement must be a Directed Acyclic Graph (DAG) , thus no cyclic relationships. If the validation fails, an exception will be thrown.

One to Many

ATK.prepare(Account.SObjectType, 10)
    .field(Account.Name).index('Name-{0000}')
    .withChildren(Contact.SObjectType, Contact.AccountId, 20)
        .field(Contact.LastName).index('Name-{0000}')
    .save();
Account Name Contact Name
Name-0001 Name-0001
Name-0001 Name-0002
Name-0002 Name-0003
Name-0002 Name-0004
... ...

Many to One

The result of the following statement is identical to the one above.

ATK.prepare(Contact.SObjectType, 20)
    .field(Contact.LastName).index('Name-{0000}')
    .withParents(Account.SObjectType, Contact.AccountId, 10)
        .field(Account.Name).index('Name-{0000}')
    .save();

Many to Many

ATK.prepare(Opportunity.SObjectType, 10)
    .field(Opportunity.Name).index('Opportunity {0000}')
    .withChildren(OpportunityContactRole.SObjectType, OpportunityContactRole.OpportunityId, 20)
        .field(OpportunityContactRole.Role).repeat('Business User', 'Decision Maker')
        .withParents(Contact.SObjectType, OpportunityContactRole.ContactId, 10)
            .field(Contact.LastName).index('Contact {0000}')
    .mock();

The above ATK statement will give the following distribution patterns, which seems not intuitive, if not intentional. It only makes scenes when the same contact play two different roles in the same opportunity.

Opportunity Name Contact Name Contact Role
Opportunity 0001 Contact 0001 Business User
Opportunity 0001 Contact 0001 Decision Maker
Opportunity 0002 Contact 0002 Business User
Opportunity 0002 Contact 0002 Decision Maker
... ... ....

Many to Many with Junction

junctionOf() can annotate an sObject as the junction of a many-to-many relationship. Its main purpose is to distribute parents of the junction object from one branch to another in a specific order. Note:

  • junctionOf() must be used directly after Entity Keywords.
  • All parent relationships of the junction sObject used in the statement must be listed in the junctionOf() keyword.
  • Different defining order of the junction relationships will result in different distributions.
ATK.prepare(Opportunity.SObjectType, 10)
    .field(Opportunity.Name).index('Opportunity {0000}')
    .withChildren(OpportunityContactRole.SObjectType, OpportunityContactRole.OpportunityId, 20)
        .junctionOf(OpportunityContactRole.OpportunityId, OpportunityContactRole.ContactId)
        .field(OpportunityContactRole.Role).repeat('Business User', 'Decision Maker')
        .withParents(Contact.SObjectType, OpportunityContactRole.ContactId, 10)
            .field(Contact.LastName).index('Contact {0000}')
    .mock();
Opportunity Name Contact Name Contact Role
Opportunity 0001 Contact 0001 Business User
Opportunity 0001 Contact 0002 Decision Maker
Opportunity 0002 Contact 0003 Business User
Opportunity 0002 Contact 0004 Decision Maker
... ... ....
Junction Keyword API Description
junctionOf(SObjectField parentId1, SObjectField parentId2); Annotate an entity is a junction of the listed parent relationships.
junctionOf(SObjectField parentId1, SObjectField parentId2, SObjectField parentId3); Annotate an entity is a junction of the listed parent relationships.
junctionOf(SObjectField parentId1, SObjectField parentId2, SObjectField parentId3, SObjectField parentId4); Annotate an entity is a junction of the listed parent relationships.
junctionOf(SObjectField parentId1, SObjectField parentId2, SObjectField parentId3, SObjectField parentId4, SObjectField parentId5); Annotate an entity is a junction of the listed parent relationships.
junctionOf(List<SObjectField> parentIds); Annotate an entity is a junction of the listed parent relationships.

📥Save

Command API

There are three commands to create the sObjects, in database, in memory, or in mock istate.

Method API Description
ATK.SaveResult save() Actual DMLs will be performed to insert/update sObjects into Salesforce.
ATK.SaveResult save(Boolean doInsert) If doInsert is false, no actual DMLs will be performed, just in-memory generated SObjects will be returned. Only writable fields can be populated.
ATK.SaveResult mock() No actual DMLs will be performed, but sObjects will be returned in SaveResult as if they are newly retrieved by SOQL with fake Ids. Both writable and read-only fields can be populated

Save Result API

Use ATK.SaveResult to retrieve sObjects generated from the ATK statement.

Method Description
List get(SObjectType objectType) Get the sObjects generated for the first SObjectType defined in ATK statement.
List get(SObjectType objectType, Integer nth); Get the sObjects generated for the nth SObjectType defined in ATK statement, i.e. child accounts in the Account Hierarchy.
List getAll(SObjectType objectType) Get all sObjects generated for SObjectType defined in ATK statement.
List getIds(SObjectType objectType) Get the sObject Ids generated for the first SObjectType defined in ATK statement.
List getIds(SObjectType objectType, Integer nth); Get the sObject Ids generated for the nth SObjectType defined in ATK statement, i.e. child accounts in the Account Hierarchy.
List getAllIds(SObjectType objectType) Get all sObject Ids generated forSObjectType defined in ATK statement.

☕ Mock

The followings are supported, when generate sObjects with mock() command:

  1. Assign extremely large fake IDs to the generated sObjects.
  2. Assign values to read-only fields, such as formula fields, rollup summary fields, and system fields.
  3. Assign one level children relationship and multiple level parent relationships.

Mock with Children

Mock Relationship To establish a relationship graph as the picture on the right, we can start from any node. However only the sObjects created in the prepare statement can have child relationships referencing their direct children.

Note: As the diagram illustrated the direct children D and E of B can not reference back to B. The decision is made to prevent a Known Salesforce Issue reported since winter 19. Here we are trying to avoid forming circular references. But D and E can still have other parent relationships, such as D to C.

All the nodes in green are reachable from node B. The diagram can be interpreted as the following SOQL statement:

SELECT Id, A__r.Id, (SELECT Id FROM E__r), (SELECT Id, C__r.Id FROM D__r) FROM B__c

And we can generate them with the following ATK statement:

ATK.SaveResult result = ATK.prepare(B__c.SObjectType, 10)
    .withParents(A__c.SObjectType, B__c.A_ID__c, 10)
    .also()
    .withChildren(D__c.SObjectType, D__c.B_ID__c, 10)
        .withParents(C__c.SObjectType, D__c.C_ID__c, 10)
        .also()
        .withChildren(F__c.SObjectType, F__c.D_ID__c, 10)
    .also(2)
    .withChildren(E__c.SObjectType, E__c.B_ID__c, 10)
    .mock();

List<B__c> listOfB = (List<B__c>)result.get(B__c.SObjectType);
for (B__c itemB : listOfB) {
    System.assertEquals(1, itemB.D__r.size());
    System.assertEquals(1, itemB.E__r.size());
}

Mock with Predefined List

Mock also supports predefined list or SOQL query results. But if there are any parent or child relationships in the predefined list, they are going to be trimmed in the generated mock sObjects.

List<B__c> listOfB = [SELECT X__r.Id, (SELECT Id FROM Y__r) FROM B__c LIMIT 3];

ATK.SaveResult result = ATK.prepare(B__c.SObjectType, listOfB)
    .withParents(A__c.SObjectType, B__c.A_ID__c, 1)
    .also()
    .withChildren(D__c.SObjectType, D__c.B_ID__c, 6)
    .mock()

List<B__c> mockOfB = (List<B__c>)result.get(B__c.SObjectType);
// The B__c in mockOfB cannot reference X__r and Y__r any more.
// The B__c in listOfB can still reference X__r and Y__r.

Fake Id

These methods are exposed in case we need manually control the ID assignments such as:

Id fakeUserId = ATK.fakeId(User.SObjectType, 1);
ATK.SaveResult result = ATK.prepare(Account.SObjectType, 9)
    .field(Account.OwnerId).repeat(fakeUserId)
    .mock()
Keyword API Description
Id fakeId(Schema.SObjectType objectType) Return self incrementing fake IDs. They will start over from each transaction, which means they are unique within each transaction. By default Ids will start from ATK.fakeId(Account.SObjectType, 1).
Id fakeId(Schema.SObjectType objectType, Integer index) Return the fake ID specified an index explicitly.

Entity Keywords

These keywords are used to establish arbitrary levels of many-to-one, one-to-many relationships. Here is a dummy example to demo the use of Entity keywords. Each of them will start a new sObject context. And it is advised to use the following indentation for clarity.

ATK.prepare(A__c.SObjectType, 10)
    .withChildren(B__c.SObjectType, B__c.A_ID__c, 10)
        .withParents(C__c.SObjectType, B__c.C_ID__c, 10)
            .withChildren(D__c.SObjectType, D__c.C_ID__c, 10)
            .also() // Go back 1 depth to C__c
            .withChildren(E__c.SObjectType, E__c.C_ID__c, 10)
        .also(2)    // Go back 2 depth to B__c
        .withChildren(F__c.SObjectType, F__c.B_ID__c, 10)
    .save();

Entity Creation Keywords

All the following APIs have an Integer size parameter at the end, which indicate how many records will be created on the fly.

ATK.prepare(A__c.SObjectType, 10)
    .withChildren(B__c.SObjectType, B__c.A_ID__c, 10)
        .withParents(C__c.SObjectType, B__c.C_ID__c, 10)
    .save();
Keyword API Description
prepare(SObjectType objectType, Integer size) Always start chain with prepare() keyword. It is the root sObject to start relationship with.
withParents(SObjectType objectType, SObjectField referenceField, Integer size) Establish many to one relationship between the previous working on sObject and the current sObject.
withChildren(SObjectType objectType, SObjectField referenceField, Integer size) Establish one to many relationship between the previous working on sObject and the current sObject.

Entity Updating Keywords

All the following APIs have a List<SObject> objects parameter at the end, which indicates the sObjects are selected/created elsewhere, and ATK will help to upsert them.

ATK.prepare(A__c.SObjectType, [SELECT Id FROM A__c]) // Select existing sObjects
    .field(A__c.Name).index('Name-{0000}')           // Update existing sObjects
    .field(A__c.Price).repeat(100)
    .withChildren(B__c.SObjectType, B__c.A_ID__c, new List<SObject> {
        new B__c(Name = 'Name-A'),                   // Manually assign field values
        new B__c(Name = 'Name-B'),
        new B__c(Name = 'Name-C')})
        .field(B__c.Counter__c).add(1, 1)            // Automatically assign field values
        .field(B__c.Weekday__c).repeat('Mon', 'Tue') // Automatically assign field values
        .withParents(C__c.SObjectType, B__c.C_ID__c, new List<SObject> {
            new C__c(Name = 'Name-A'),
            new C__c(Name = 'Name-B'),
            new C__c(Name = 'Name-C')})
    .save();
Keyword API Description
prepare(SObjectType objectType, List<SObject> objects) Always start chain with prepare() keyword. It is the root sObject to start relationship with.
withParents(SObjectType objectType, SObjectField referenceField, List<SObject> objects) Establish many to one relationship between the previous working on sObject and the current sObject.
withChildren(SObjectType objectType, SObjectField referenceField, List<SObject> objects) Establish one to many relationship between the previous working on sObject and the current sObject.

Entity Reference Keywords

All the following APIs don't have a third parameter of size or list at the end, which means the relationship will look back to reference the previously created sObjects.

Keyword API Description
withParents(SObjectType objectType, SObjectField referenceField) Establish many to one relationship between the previous working on sObject and the current sObject.
withChildren(SObjectType objectType, SObjectField referenceField) Establish one to many relationship between the previous working on sObject and the current sObject.

Note: Once these APIs are used, please make sure there are sObjects with the same type created previously, and only created once.

Field Keywords

These keywords are used to generate field values based on simple rules automatically.

ATK.prepare(A__c.SObjectType, 10)
    .withChildren(B__c.SObjectType, B__c.A_ID__c, 10)
        .field(B__C.Name__c).index('Name-{0000}')
        .field(B__C.PhoneNumber__c).index('+86 186 7777 {0000}')
        .field(B__C.Price__c).repeat(12.34)
        .field(B__C.CampanyName__c).repeat('Google', 'Apple', 'Microsoft')
        .field(B__C.Counter__c).add(1, 1)
        .field(B__C.StartDate__c).addDays(Date.newInstance(2020, 1, 1), 1)
    .save();

Basic Field Keywords

Keyword API Description
index(String format) Formatted string with {0000}, can recognize left padding. i.e. Name-{0000} will generate Name-0001, Name-0002, Name-0003 etc.
Repeat Family
repeat(Object value) Repeat with a single fixed value.
repeat(Object value1, Object value2) Repeat with the provided values alternatively.
repeat(Object value1, Object value2, Object value3) Repeat with the provided values alternatively.
repeat(Object value1, Object value2, Object value3, Object value4) Repeat with the provided values alternatively.
repeat(Object value1, Object value2, Object value3, Object value4, Object value5) Repeat with the provided values alternatively.
repeat(List<Object> values) Repeat with the provided values alternatively.**
RepeatX Family
repeatX(Object value1, Integer size1, Object value2, Integer size2) repeat each value by x, y... times in sequence.
repeatX(Object value1, Integer size1, Object value2, Integer size2, Object value3, Integer size3) repeat each value by x, y... times in sequence.
repeatX(Object value1, Integer size1, Object value2, Integer size2, Object value3, Integer size3, Object value4, Integer size4) repeat each value by x, y... times in sequence.
repeatX(Object value1, Integer size1, Object value2, Integer size2, Object value3, Integer size3, Object value4, Integer size4, Object value5, Integer size5) repeat each value by x, y... times in sequence.
repeatX(List<Object> values, List<Integer> sizes) repeat each value by x, y... times in sequence.

Arithmetic Field Keywords

These keywords will increase/decrease the init values by the provided steps.

Number Arithmetic

Keyword API Description
add(Decimal init, Decimal step) Must be applied to a number type field.
substract(Decimal init, Decimal step) Must be applied to a number type field.
divide(Decimal init, Decimal factor) Must be applied to a number type field.
multiply(Decimal init, Decimal factor) Must be applied to a number type field.

Date/Time Arithmetic

Keyword API Description
addYears(Object init, Integer step) Must be applied to a Datetime/Date type field.
addMonths(Object init, Integer step) Must be applied to a Datetime/Date type field.
addDays(Object init, Integer step) Must be applied to a Datetime/Date type field.
addHours(Object init, Integer step) Must be applied to a Datetime/Time type field.
addMinutes(Object init, Integer step) Must be applied to a Datetime/Time type field.
addSeconds(Object init, Integer step) Must be applied to a Datetime/Time type field.

Lookup Field Keywords

These are field keywords in nature, but without the need to be chained after .field(). ATK will help to look up the IDs and assign them to the correct relationship fields automatically.

ATK.prepare(Account.SObjectType, 10)
    .recordType('Business_Account');   // case sensitive

ATK.prepare(User.SObjectType, 10)
    .profile('Chatter Free User')      // must be applied to User SObject
    .permissionSet('Survey_Creator');  // must be applied to User SObject
Keyword API Description
recordType(String name) Assign record type ID by developer name, the name is case sensitive due the getRecordTypeInfosByDeveloperName() API.
profile(String name) Assign profile ID by profile name.
permissionSet(String name) Assign the permission set to users by developer name.
permissionSet(String name1, String name2) Assign all the permission sets to users by developer names.
permissionSet(String name1, String name2, String name3) Assign all the permission sets to users by developer names.
permissionSet(List<String> names) Assign all the permission sets to users by developer names.

Entity Builder Factory

In order to increase the reusability of ATK, we can abstract the field keywords into an Entity Builder.

@IsTest
public with sharing class CampaignServiceTest {
    @TestSetup
    static void setup() {
        ATK.SaveResult result = ATK.prepare(Campaign.SObjectType, 4)
            .build(EntityBuilderFactory.campaignBuilder)     // Reference to Entity Builder
            .withChildren(CampaignMember.SObjectType, CampaignMember.CampaignId, 8)
                .withParents(Lead.SObjectType, CampaignMember.LeadId, 8)
                    .build(EntityBuilderFactory.leadBuilder) // Reference to Entity Builder
            .save();
    }
}
@IsTest
public with sharing class EntityBuilderFactory {
    public static CampaignEntityBuilder campaignBuilder = new CampaignEntityBuilder();
    public static LeadEntityBuilder leadBuilder = new LeadEntityBuilder();

    // Inner class implements ATK.EntityBuilder
    public class CampaignEntityBuilder implements ATK.EntityBuilder {
        public void build(ATK.Entity campaignEntity, Integer size) {
            campaignEntity
                .field(Campaign.Type).repeat('Partners')
                .field(Campaign.Name).index('Name-{0000}')
                .field(Campaign.StartDate).repeat(Date.newInstance(2020, 1, 1))
                .field(Campaign.EndDate).repeat(Date.newInstance(2020, 1, 1));
        }
    }

    // Inner class implements ATK.EntityBuilder
    public class LeadEntityBuilder implements ATK.EntityBuilder {
        public void build(ATK.Entity leadEntity, Integer size) {
            leadEntity
                .field(Lead.Company).index('Name-{0000}')
                .field(Lead.LastName).index('Name-{0000}')
                .field(Lead.Email).index('test.user+{0000}@email.com')
                .field(Lead.MobilePhone).index('+86 186 7777 {0000}');
        }
    }
}

License

Apache 2.0