19 min read

Trigger Best Practices Part 2 - Unit Testing

It is worth mentioning that in most other development environments there isn't anything enforcing the use of unit testing other than internal policies or personal preference. However, Salesforce requires unit testing before code can be deployed to a production environment. This policy is enforced by requiring that you have written a test class or method that is able to successfully execute at least 75% of the code you have written. For more information on unit testing you can review the Force.com Documentation on unit testing.

So with all that in mind let's at least take a moment to talk about the benefits unit testing can have for us. For one it provides us with means to validate that our code is working as expected. As we get requests to modify a given method's functionality we can execute these tests to validate that our original functionality was not affected. For those that develop in a team environment this can be very valuable because you may be unaware of other functionality in the system that your updates might have an impact on. By producing unit tests you now have the ability to focus on the functionality you need to develop and let the tests discover any impact to other parts of the system.

Use Separate Classes for Test Methods

The first tip I would like to share is how to decided whether you should have a separate test class or just add test methods to your class.

Adding test methods to your classes inline with your functional methods is possible and as long as you use the right attributes (i.e. isTest ) then you are ok. However, as a best practice you want to keep your test classes separate from your logic.

Proper testing requires quite a bit of code to setup the data for multiple scenarios. Applying all of this setup logic inline with your classes can be a nightmare for readability and maintenance. As someone who has tried both approaches, I can tell you that inline test methods can be painful to maintain and refactor; not only for you as the author of the code but for others that may be reading the code to understand and/or modify it.

In the coming examples it will become more apparent how much code is actually needed to properly test your functionality and how even the simplest of solutions benefits from having a separate class for unit test methods.

Unit Tests != Bullet Proof Code

I will likely expand on this topic with a separate post because there is a lot to cover. But it's worth at least summarizing this topic before we get into the unit test examples and scenarios.

Unit tests do not equal bullet proof code. It is entirely possible (and common) that you could write unit tests that pass with 100% code coverage that will still have a bug in them.

As developers, we know how the code is supposed to be used and it is this bias that tends to blind us from scenarios that might put the system in a state that causes an error in our code. It is natural and expected since it is improbable that we can imagine every way a user could use our solution. So as a tip, where possible, it is good to have someone else test your code and not rely on unit tests to validate your solutions functionality.

Use Start and Stop test methods

In addition to testing the functionality of our code we are also validating that our code is executing within the limits of the system. As our code is executed, Salesforce.com keeps track of how many lines of code are processed, how many queries are made, etc.

When setting up unit test data it means we will be doing additional inserts, updates, and/or queries that we don't want included in those totals. The best way to ensure that the test setup code is not included in the execution totals is to make sure to use the Test.startTest() and Test.stopTest() methods to give the platform some context on when your actual testing is beginning and when it ends.

There are also other reasons to use start and stop methods for unit tests such as validating future and scheduled method calls but that is topic for another time.

Naming Conventions

There are no standard conventions for naming your unit test classes or methods. I prefer to name my unit test classes after the class that I am testing and appending "_Test" to the name. Other options that I have seen prefix classes with the word "Test". Either is ok, what is important is that the name at least identifies itself as a test class so that it can be easily referenced.

Test methods also do not have a set naming convention. You do have to use attributes such as isTest or testMethod when defining the method itself but the name can be whatever you like. I prefer to name methods in a way that identifies the method being tested such as scenario name prefixed with the word test. For example: testSumAmounts() or testNewAccountProcess().

Unit Testing - Example Scenarios

Now that we covered a couple of tips to get us started let's get right into some examples to start illustrating how these best practices can be put to use.

Note: The scenarios below will utilize the best-practices described in the previous article. So each example will consist of 3 classes: Trigger Class, Code Class, and Test Class.

Scenarios Covered:

  • Scenario 1 - A simple roll-up summary trigger
  • Scenario 2 - Applying business rules
  • Scenario 3 - Bulk Triggers

Scenario 1. A simple roll-up summary trigger

While Salesforce does support roll-up summary fields there are scenarios where you may need to write your own. It could be because the current toolset doesn't support the filter you need or that you have already reached the limit on the number of roll-up fields that you can have. So we will look at the best way to create and test such a trigger.

To setup the scenario let us assume that we are at our limit of roll-up fields and thus we need to implement a custom roll-up using a trigger for a parent-child relationship of objects. Also, for simplicity we will use the Opportunity and OpportunityLineItem objects in this scenario.

In this scenario we need to consider a few things:

  1. We want the roll-up field to be read-only to all users except the system administrator profile called Rollup Amount (API Name: Rollup_Amount__c )
  2. We need to update the roll-up field any time a child record is created, updated, deleted.
  3. We need to support bulk DML
  4. For efficiency we will perform the actual roll-up using an aggregate query against the total price so that the quantity multiplier is taken into account.

First, lets build our trigger. Remember that we will be utilizing the best practices described in part 1 of this series. Our trigger will require access to all record ids so we will need to use the after-trigger context for insert, update, and delete.

trigger OpportunityLineItemTrigger on OpportunityLineItem (before insert
	,before update
	,before delete
	,after insert
	,after update
	,after delete) {

	if (trigger.isAfter && trigger.isDelete == false) {
		OpportunityRollups.rollupLineItems(trigger.new);
	}
	else if (trigger.isAfter && trigger.isDelete == true) {
		OpportunityRollups.rollupLineItems(trigger.old);
	}
}

Second, lets build our class that will contain all of our execution logic. Make sure to pay attention to the use of the without sharing keyword in the class definition. This will allow our code to update the roll-up field under a system administrator context.

public without sharing class OpportunityRollups {
	public static void rollupLineItems(List<OpportunityLineItem> lineItems) {
		if (lineItems != null && lineItems.isEmpty() == false) {
			//Get Opportunity Ids
			Set<Id> opportunityIds = new Set<Id>();
			
			for (OpportunityLineItem lineItem : lineItems) {
				opportunityIds.add(lineItem.OpportunityId);
			}

			//Get Map of Opportunities
			Map<Id, Opportunity> opportunities = new Map<Id, Opportunity>([SELECT Id, Rollup_Amount__c FROM Opportunity WHERE Id IN :opportunityIds]);

			//Aggregate Rollup_Amount__c
			Map<Id, double> oppAmounts = new Map<Id, double>();

			AggregateResult[] results = [SELECT OpportunityId, SUM(TotalPrice) RollupAmount FROM OpportunityLineItem WHERE OpportunityId IN :opportunityIds GROUP BY OpportunityId];
			for (AggregateResult result : results) {
				Id opportunityId = (Id) result.get('OpportunityId');
				double rollupAmount = (double) result.get('RollupAmount');

				oppAmounts.put(opportunityId, rollupAmount);
			}

			//Map Amounts for Update
			List<Opportunity> oppsToUpdate = new List<Opportunity>();

			for(Id opportunityId : opportunities.keySet()) {
				Opportunity opp = opportunities.get(opportunityId);
				
				double rollupAmount = 0;

				if (oppAmounts.containsKey(opportunityId)) {
					rollupAmount = oppAmounts.get(opportunityId);
				}

				if (rollupAmount != opp.Rollup_Amount__c) {
					opp.Rollup_Amount__c = rollupAmount;
					oppsToUpdate.add(opp);
				}
			}

			//Update Opportunities
			if (oppsToUpdate.isEmpty() == false) {
				update oppsToUpdate;
			}
		}
	}
}

Finally, lets build our unit tests.

@isTest(SeeAllData=true)
private class OpportunityRollups_Test {
	
	static testMethod void testLineItemRollup_Insert() {
		//Setup Test
		OpportunityLineItem testLineItem = getTestOpportunityLineItem();
		insert testLineItem;

		//Start Test
		Test.startTest();

		OpportunityRollups.rollupLineItems(new List<OpportunityLineItem>{ testLineItem });

		Test.stopTest();

		//Validate Results
		Opportunity testResult = [SELECT Id, Rollup_Amount__c FROM Opportunity WHERE Id =: testLineItem.OpportunityId];

		System.assertEquals(200, testResult.Rollup_Amount__c, 'Expected the Opportunity.Rollup_Amount__c field to be updated based on rollup of Opportunity.UnitPrice');
	}

	static testMethod void testLineItemRollup_Update() {
		//Setup Test
		OpportunityLineItem testLineItem = getTestOpportunityLineItem();
		insert testLineItem;

		testLineItem.Quantity = 5;
		update testLineItem;

		//Start Test
		Test.startTest();

		OpportunityRollups.rollupLineItems(new List<OpportunityLineItem>{ testLineItem });

		Test.stopTest();

		//Validate Results
		Opportunity testResult = [SELECT Id, Rollup_Amount__c FROM Opportunity WHERE Id =: testLineItem.OpportunityId];

		System.assertEquals(500, testResult.Rollup_Amount__c, 'Expected the Opportunity.Rollup_Amount__c field to be updated based on rollup of Opportunity.UnitPrice');
	}

	static testMethod void testLineItemRollup_Delete() {
		//Setup Test
		OpportunityLineItem testLineItem = getTestOpportunityLineItem();
		insert testLineItem;

		delete testLineItem;

		//Start Test
		Test.startTest();

		OpportunityRollups.rollupLineItems(new List<OpportunityLineItem>{ testLineItem });

		Test.stopTest();

		//Validate Results
		Opportunity testResult = [SELECT Id, Rollup_Amount__c FROM Opportunity WHERE Id =: testLineItem.OpportunityId];

		System.assertEquals(0, testResult.Rollup_Amount__c, 'Expected the Opportunity.Rollup_Amount__c field to be updated based on rollup of Opportunity.UnitPrice');
	}

	private static OpportunityLineItem getTestOpportunityLineItem() {
		Account testAccount = new Account();
		testAccount.Name = 'Test Account';
		insert testAccount;

		Opportunity testOpp = new Opportunity();
		testOpp.AccountId = testAccount.Id;
		testOpp.Name = 'Test Opp';
		testOpp.StageName = 'Prospecting';
		testOpp.CloseDate = System.today() + 5;
		testOpp.Rollup_Amount__c = 255; //Supply a dummy value to make sure it gets updated
		insert testOpp;

		Product2 testProduct = new Product2();
		testProduct.Name = 'Test Product';
		testProduct.ProductCode = 'TestP1';
		insert testProduct;

		Pricebook2 standardPricebook = [SELECT Id FROM Pricebook2 WHERE IsActive=true AND IsStandard=true LIMIT 1];

		PricebookEntry testPricebookEntry = new PricebookEntry();
		testPricebookEntry.Pricebook2Id = standardPricebook.Id;
		testPricebookEntry.Product2Id = testProduct.Id;
		testPricebookEntry.UnitPrice = 100;
		testPricebookEntry.IsActive = true;
		insert testPricebookEntry;

		OpportunityLineItem testLineItem = new OpportunityLineItem();
		testLineItem.OpportunityId = testOpp.Id;
		testLineItem.PricebookEntryId = testPricebookEntry.Id;
		testLineItem.Quantity = 2;
		testLineItem.UnitPrice = 100;

		return testLineItem;
	}
}

Even though this is a simple example of a roll-up trigger you can easily see how you can come up with a number of scenarios to test. I chose to create a test method for each DML operation. I also took the time to validate the results of the calculation for each operation. Of course, I could have created many more tests to validate different numbers, both positive and negative, but since I am using an aggregate query I felt confident that the SUM() method is working as expected.

As with most things, you have to find a balance. You benefit most by writing tests that do more than the bare minimum needed to get 75% code coverage; but take care that you do not put yourself in a position where you are attempting to test every conceivable scenario. Stay focused on the most critical portions of your code.

OpportunityLineItemTrigger Class

As you can see this trigger class is very simple. We use some properties from the trigger object to determine what context the trigger is executing under and then use that to decide if we should run our code or not. In this case we only wanted to execute the trigger during after events and we also needed to make sure to pass in the right list of records from the trigger object. For instance, delete triggers present their data in the trigger.old property while the other event types utilize the trigger.new property.

OpportunityRollups Class

The OpportunityRollups class is where we put all of our logic for the solution. Since our logic is simple and isolated enough we can get away with implementing a single static method to support our requirements.

Notice that rollupLineItems() method is designed so that we can pass in the list of OpportunityLineItem objects to process. This allows us to implement the code in a way that supports insert, update, and delete events. It also allows us to remove any dependency on the trigger itself and just pass in the data to test. Should we ever need to deactivate the trigger the unit tests should still pass since we are calling the method directly and not relying on the DML operation for testing.

Also, notice that method does what it can to see if the value on the Opportunity needs to be changed by comparing the current and calculated values. While it does take more work and code to do this type of validation, as more events get added to an Opportunity avoiding unnecessary DML operations will be good for the overall health and performance of the system.

OpportunityRollups_Test

First lets discuss the @isTest(SeeAllData=true) attribute. The latest version of the Force.com platforms testing framework hides all existing data in the system from your unit test context. By adding the (SeeAllData=true) portion to the attribute, the system will make existing data in the system available to your unit tests.

As a best practice you should create all of the data you need for testing from within your test method. This avoids any dependency on data that is in the system making your unit tests much more effective and portable between environments.

The reason the attribute is being used in this context is because you cannot create certain objects from within a unit test context. In this case, we cannot create a pricebook which is required to be able to create a pricebook entry for use with the OpportunityLineItem object. So we need to access an existing pricebook in order to conduct our tests.

Next, let's look at how the data required for testing is generated. I realized after generating my first test method that I would need the same kind of data for each one of my scenarios. So I created a method that could be re-used across my different test methods called getTestOpportunityLineItem(). If I had other data requirements for this particular scenario then I would have created another setup helper method.

The getTestOpportunityLineItem() method creates and inserts all of the test data I need with the exception of the OpportunityLineItem object. Instead, it returns an instance of that object with the required data populated. This gives me the flexibility to modify the object before inserting or to insert in between the start/stop methods.

Finally, the test methods themselves are fairly straightforward as you can see. Each method is dedicated to testing a specific DML operation and then validating that the proper value is set at the Opportunity level.

I will admit that these tests are bit tricky to understand because when the OpportunityLineItem is inserted, updated, or deleted the trigger is executed and thus the method is called. However, in the test method I choose to call the method directly anyway. This is because I am not relying on the trigger to execute the methods logic. Should the trigger ever need to be disabled the tests will still execute the method and pass. In other scenarios, where querying data is not a dependency, methods can be tested without executing a DML operation making your tests simpler and faster.

Scenario 2. Applying business rules

Applying business logic to data being updated or added is a big topic. But lets just go with an easy scenario that sets the priority of a case based on the type of a given account. The objects we will use in this scenario will be the Account and Case objects.

This particular scenario allows us to cover the topic of re-use because we need two triggers to meet the requirements of this request. First, we need a trigger that executes when a case is created that will default the priority of the case based on the account. Then, we will need a trigger on the account that will update any related case priority when the account type is changed.

In this scenario we need to consider a few things:

  1. We need to only execute the code when the account type field changes
  2. We need to update all open cases associated to a given account
  3. We are going to assume that the Case.Priority field is read-only for all users except the system administrator. Which would be recommended if the priority is going to be controller by the account type.
  4. We will map the priority using the following account types: VIP = High, Customer = Medium, Prospect = Low, and any other values will be defaulted to Low.

First, lets build our triggers.

trigger CaseTrigger on Case (before insert
	,before update
	,before delete
	,after insert
	,after update
	,after delete) {

	if (trigger.isBefore && trigger.isInsert) {
		CasePriority.setCasePriority(trigger.new);
	}
}

Second, lets build our class that will contain all of our execution logic. Make sure to pay attention to the use of the without sharing keyword in the class definition. This will allow our code to update the priority field under a system administrator context.

public without sharing class CasePriority {
	public static final string PriorityHigh = 'High';
	public static final string PriorityMedium = 'Medium';
	public static final string PriorityLow = 'Low';

	public static final string TypeVIP = 'VIP';
	public static final string TypeCustomer = 'Customer';
	public static final string TypeProspect = 'Prospect';

	public static string getCasePriority(string accountType) {
		if (String.IsBlank(accountType)) {
			return PriorityLow;
		}
		else if (TypeVIP.equalsIgnoreCase(accountType)) {
			return PriorityHigh;
		}
		else if (TypeCustomer.equalsIgnoreCase(accountType)) {
			return PriorityMedium;
		}
		else if (TypeProspect.equalsIgnoreCase(accountType)) {
			return PriorityLow;
		}
		else {
			return PriorityLow;
		}
	}

	public static void setCasePriority(List<Case> cases) {
		if (cases != null && cases.isEmpty() == false) {
			//Get Account Info
			Set<Id> accountIds = new Set<Id>();

			for (Case caseItem : cases) {
				if (caseItem.AccountId != null) accountIds.add(caseItem.AccountId);
			}

			Map<Id, Account> accounts = new Map<Id, Account>([SELECT Id, Type FROM Account WHERE Id IN :accountIds]);

			//Set Priority
			for (Case caseItem : cases) {
				Id accountId = caseItem.AccountId;

				string accountType = '';

				if (accountId != null && accounts.containsKey(accountId)) {
					accountType = accounts.get(accountId).Type;
				}

				caseItem.Priority = getCasePriority(accountType);
			}
		}
	}

	public static void updateAccountCasePriority(List<Account> accounts, Map<Id,Account> oldMap) {
		if (accounts != null && accounts.isEmpty() == false) {
			//Identified Modified Accounts
			List<Id> modifiedAccountIds = new List<Id>();
			
			for (Account acct : accounts) {
				string originalType = '';
				
				if (oldMap != null && oldMap.containsKey(acct.Id)) {
					originalType = oldMap.get(acct.Id).Type;
				}

				if (originalType != acct.Type) {
					modifiedAccountIds.add(acct.Id);
				}
			}

			//Get Account Cases
			List<Case> casesToUpdate = new List<Case>();

			List<Case> cases = [SELECT Id, Priority, Account.Type FROM Case WHERE AccountId IN :modifiedAccountIds];
			for(Case caseItem : cases) {
				string casePriority = String.IsBlank(caseItem.Priority) ? '' : caseItem.Priority;
				string acctPriority = getCasePriority(caseItem.Account.Type);

				if (acctPriority.equalsIgnoreCase(casePriority) == false) {
					caseItem.Priority = acctPriority;
					casesToUpdate.add(caseItem);
				}
			}

			//Update Cases
			if (casesToUpdate.isEmpty() == false) {
				update casesToUpdate;
			}
		}
	}
}

Finally, lets build our unit tests.

@isTest
private class CasePriority_Test {
	
	static testMethod void testGetCasePriority() {
		//Start Test
		Test.startTest();

		string testNull = CasePriority.getCasePriority(null);
		string testVIP = CasePriority.getCasePriority(CasePriority.TypeVIP);
		string testCustomer = CasePriority.getCasePriority(CasePriority.TypeCustomer);
		string testProspect = CasePriority.getCasePriority(CasePriority.TypeProspect);
		string testUnknown = CasePriority.getCasePriority('Some Unknown Type');

		Test.stopTest();

		//Validate Test
		System.assertEquals(CasePriority.PriorityLow, testNull);
		System.assertEquals(CasePriority.PriorityHigh, testVIP);
		System.assertEquals(CasePriority.PriorityMedium, testCustomer);
		System.assertEquals(CasePriority.PriorityLow, testProspect);
		System.assertEquals(CasePriority.PriorityLow, testUnknown);
	}

	static testMethod void testSetCasePriority() {
		//Setup Test
		Account testAccount = new Account();
		testAccount.Name = 'Test Account';
		testAccount.Type = CasePriority.TypeVIP;
		insert testAccount;

		Case testCase = new Case();
		testCase.AccountId = testAccount.Id;
		testCase.Priority = 'Low';
		insert testCase;

		//Start Test
		Test.startTest();

		CasePriority.setCasePriority(new List<Case>{ testCase });

		Test.stopTest();

		//Validate Test
		System.assertEquals(CasePriority.PriorityHigh, testCase.Priority);
	}

	static testMethod void testUpdateAccountCasePriority() {
		//Setup Test
		Account testAccount = new Account();
		testAccount.Name = 'Test Account';
		testAccount.Type = CasePriority.TypeVIP;
		insert testAccount;

		Case testCase = new Case();
		testCase.AccountId = testAccount.Id;
		testCase.Priority = 'Low';
		insert testCase;

		Account testAccount2 = new Account(Id=testAccount.Id);
		testAccount2.Type = CasePriority.TypeCustomer;
		update testAccount2;

		Map<Id, Account> oldMap = new Map<Id, Account>{testAccount.Id => testAccount};

		//Start Test
		Test.startTest();

		CasePriority.updateAccountCasePriority(new List<Account>{ testAccount2 }, oldMap);

		Test.stopTest();

		//Validate Test
		Case caseResult = [SELECT Id, Priority FROM Case WHERE Id = :testCase.Id];
		System.assertEquals(CasePriority.PriorityMedium, caseResult.Priority);
	}
}

Again, simple requirements have resulted in quite a bit of code but since we realized we had an opportunity for some reuse we were able to simplify our testing.

CaseTrigger and AccountTrigger Classes

In this scenario we are actually operating under two different contexts. For the CaseTrigger we are utilizing the before event context for new cases so that we won't need to perform a DML operation to set the case priority. In the case of the AccountTrigger we are only concerned with the after event context for accounts that have been updated.

Notice that in the AccountTrigger I am passing along the trigger.oldMap property to the method. This is so that I can do a comparison of the original account type to the current account type and determine if the values have changed before executing any additional logic for a given account.

CasePriority Class

We have a number of best practices implemented within this class. Let's start with the static variables (constants) defined at the start of the class. I don't like using hardcoded text in my code and where possible I prefer to store these values in a Custom Setting object so that they are configurable. However, in many cases that is just unnecessary overhead, so a good alternative is to make them constants. Not only does it allow you to change the value easily but in cases where the value is a bit cryptic it can make your code much more readable to others.

To meet our requirements I found that three methods would be sufficient with one of those methods providing a reusable way to determine the case priority based on an account type. Should this logic ever need to be modified I can change this single method and account for both trigger implementations.

The CaseTrigger implementation is unique in that we are relying on the before event context. This means that we are going to be able to avoid the need for a DML/Update command to the case because we will simply set the value and allow the system to commit when its done processing the trigger. Our unit test also accounts for this and simply checks the value on the object rather than querying the case to validate the value.

The AccountTrigger implementation does require a DML operation to update any related cases but we want to make sure we only do this when the account type field has been changed. That is why we added the oldMap to the method arguments allowing us to walk through the accounts and validate that it has changed before updating any cases.

CasePriority_Test Class

The testGetCasePriority() method is a great example of why breaking up your methods into smaller chunks not only allows you to reuse blocks of functionality but allows you to focus your testing for that functionality. In this case we are able to pass in not only the values we are expecting but also validate what happens when we pass data that we are not expecting, such as null or some random text.

As was the case for our first scenario we are calling the methods directly even though we are performing a DML operation that will execute these methods. Again, our goal with this pattern is to reduce our dependency on the trigger and should the trigger be disabled allow our tests to pass.

Scenario 3. Bulk Triggers

For this scenario we will focus more on snippets rather than complete solutions. As discussed in Part 1. triggers must be bulk enabled, meaning that they should be written to support more than one record. In most cases when a user is updating a record in the UI your trigger will execute for that single record but there are other trigger events such as mass updates through the UI or API that need to be accounted for.

Understanding this little fact about triggers is key to how you approach implementing and testing your trigger logic. Our main concern is going to be to make sure that we are within the limits of the system.

For instance the following code will execute just fine for a single record but will fail for anything over 100 (the current SOQL limit).

public with sharing class ContactHelper {
	
	public static void setDefaultValues(List<Contact> contacts) {
		for (Contact contact : contacts) {
			if (contact.AccountId != null) {
				Account account = [SELECT Id, BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry FROM Account WHERE Id = :contact.AccountId];
				
				contact.MailingStreet = account.BillingStreet;
				contact.MailingCity = account.BillingCity;
				contact.MailingState = account.BillingState;
				contact.MailingPostalCode = account.BillingPostalCode;
				contact.MailingCountry = account.BillingCountry;
			}
		}
	}
}

The best way to avoid this is to make sure your code is written to support bulk events. For instance the previous example can be re-written to the following and resolve the issue:

public with sharing class ContactHelper {
	
public static void setDefaultValues(List<Contact> contacts) {
		Set<Id> accountIds = new Set<Id>();

		for (Contact contact : contacts) {
			if (contact.AccountId != null) accountIds.add(contact.AccountId);
		}

		Map<Id, Account> accounts = new Map<Id, Account>([SELECT Id, BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry FROM Account WHERE Id IN :accountIds]);

		for (Contact contact : contacts) {
			if (contact.AccountId != null && accounts.containsKey(contact.AccountId)) {
				Account account = accounts.get(contact.AccountId);
				
				contact.MailingStreet = account.BillingStreet;
				contact.MailingCity = account.BillingCity;
				contact.MailingState = account.BillingState;
				contact.MailingPostalCode = account.BillingPostalCode;
				contact.MailingCountry = account.BillingCountry;
			}
		}	
	}
}

Unit Testing Bulk Triggers

Unit testing bulk triggers is fairly simple. You simply create a sufficient amount of test data for your unit test to work with. For instance, using the example above you would setup your test data similar to the sample unit test class below:

@isTest
private class ContactHelper_Test {
	static testMethod void testSetDefaultValues() {
		//Setup Test
		Account testAccount = new Account();
		testAccount.Name = 'Test Account';
		testAccount.BillingStreet = 'My Street';
		insert testAccount;

		Contact testContact = new Contact();
		testContact.AccountId = testAccount.Id;
		testContact.FirstName = 'Test';
		testContact.LastName = 'User';

		//Run Test
		Test.startTest();

		ContactHelper.setDefaultValues(new List<Contact>{ testContact });

		Test.stopTest();

		//Validate Test
		System.assertEquals(testAccount.BillingStreet, testContact.MailingStreet);
	}
}

Given that every trigger must be bulk enabled an obvious tip would be to make sure that all of your tests are implemented using multiple records. However, there is an issue with doing this. The Force.com platform is extremely slow at running your unit tests, especially in environments where you are doing any significant customizations such as triggers, workflow rules, etc.

The main reason this is an issue is when you deploy your code to production it executes all tests in the system. Which depending on how many unit test classes, methods, and test data required for each test this can add up.

Don't let this discourage you from doing what is right though. Salesforce is working on this particular issue and does have some stuff coming that should give us some options for minimizing the impact unit testing performance has on our deployments. So for now let me share with you how I attempt to minimize this issue. It isn't perfect and doesn't solve the issue entirely but it does give me a way to scale my testing back a bit.

Tip 1. Choose how to bulk test

I typically start with unit test scenarios based on a single record and with the perspective of validating the logic. This allows me to setup a simple data model for each scenario I am testing. Then I dedicate a couple of methods specifically for bulk events. In some cases I retest all scenarios and in others I use a single scenario that is able to execute the most lines of code in a bulk event context. This can cut back on the amount of time it takes to test your code because out of say 2-5 scenarios only one is executing under a bulk context (again, it's about finding a good balance).

Tip 2. Scale your test for production

One thing that I do when creating bulk enabled unit tests is to make the number of records I create dynamic. Meaning, I create a variable that determines how many records should be created. This allows me to set the value high for load testing and to scale it back to something reasonable for a production release. If our tests are written properly and our development/test environments are similar enough then we are relatively safe in assuming that a high volume test that passed in dev/test should work just fine in production. Thus, we can scale back the number of test records to improve the performance of our deployment package.

Just know that since you scaled back the number records to test in production any new code that potentially adds to transactions executed along side your existing trigger logic could have an impact that might not be evident since it will only test with a smaller set of records.


On the plus side, should this ever be an issue you can just change the value to your high volume number and deploy.

Here is how the volume variable can be used in a testing scenario:

@isTest
private class ContactHelper_Test {
	private static final boolean highVolume = true;

	static testMethod void testSetDefaultValues_Bulk() {
		//Setup Test
		integer volume = highVolume ? 150 : 10;

		List<Account> testAccounts = new List<Account>();
		for (integer i=0; i < volume; i++) {
			Account testAccount = new Account();
			testAccount.Name = 'Test Account' + i.format();
			testAccount.BillingStreet = 'My Street';

			testAccounts.add(testAccount);
		}
		insert testAccounts;

		List<Contact> testContacts = new List<Contact>();
		for (Account testAccount : testAccounts) {
			Contact testContact = new Contact();
			testContact.AccountId = testAccount.Id;
			testContact.FirstName = 'Test';
			testContact.LastName = 'User';

			testContacts.add(testContact);
		}

		//Run Test
		Test.startTest();

		ContactHelper.setDefaultValues2(testContacts);

		Test.stopTest();

		//Validate Test
		Map<Id, Account> accounts = new Map<Id, Account>(testAccounts);

		for (Contact testContact : testContacts) {
			Account testAccount = accounts.get(testContact.AccountId);
			System.assertEquals(testAccount.BillingStreet, testContact.MailingStreet);
		}
	}
}

Notice that I have a boolean variable on my class that enables high volume testing. Then for each of the test methods I set what the volume should be based on this. This is to allow me to customize the volume based on the scenario. For example, I could determine that 100 is an acceptable volume for one scenario and 200 for another. This particular tip is rooted in keeping things simple and easy because if you had to go through and change all of your values for each unit test method then you are less likely to do it. So by making this trivial you are more likely to enable high volumes for regression testing.

Summary

We covered a lot in this article and I hope you found it beneficial. It was certainly a challenge to pick only a handful of scenarios, but I felt these covered the most common scenarios that you are likely to face.

As always I encourage any questions, comments, or even ideas for topics that you would like for me to cover.