New Spring ’15 Feature: @testSetup

The Spring ’15 release is approaching quickly! As with every Salesforce release, it comes with a plethora of new features. Over the next few weeks, I am going to outline what I find to be some of the cooler features.

@testSetup

One of the nicest features of Spring ’15 in my mind is @testSetup. From the release notes:

Use test setup methods (methods that are annotated with @testSetup) to create test records once and then access them in every test method in the test class. Test setup methods can be time-saving when you need to create reference or prerequisite data for all test methods, or a common set of records that all test methods operate on.

Test setup methods can reduce test execution times especially when you’re working with many records. Test setup methods enable you to create common test data easily and efficiently. By setting up records once for the class, you don’t need to re-create records for each test method. Also, because the rollback of records that are created during test setup happens at the end of the execution of the entire class, the number of records that are rolled back is reduced. As a result, system resources are used more efficiently compared to creating those records and having them rolled back for each test method.

If a test class contains a test setup method, the testing framework executes the test setup method first, before any test method in the class. Records that are created in a test setup method are available to all test methods in the test class and are rolled back at the end of test class execution. If a test method changes those records, such as record field updates or record deletions, those changes are rolled back after each test method finishes execution. The next executing test method gets access to the original unmodified state of those records.

In Action

Let’s consider the following class:

[java]
public class AccountService {
public static List getAllAccounts(){
return [SELECT Id, Name FROM Account LIMIT 500];
}

public static List getOpportunitiesOnAccount(Id accountId){
return [SELECT Id, Name FROM Opportunity WHERE AccountId = :accountId];
}

public static void updateAccountName(Account account, String accountName){
account.Name = accountName;
update account;
}
}
[/java]

Typically, this would require a unit test class that looks like:

[java]
@isTest
public class AccountServiceTest {
static testMethod void testGetAllAccounts(){
List accounts = new List();
for(Integer i=0;i < 10;i++){ accounts.add(new Account(Name = 'Test Acc')); } insert accounts; Test.startTest(); List accountsFromService = AccountService.getAllAccounts();
Test.stopTest();

System.assertEquals(10, accountsFromService.size(), ‘There should be 10 accounts’);
}

static testMethod void testGetOpportunitiesOnAccount(){
Account acc = new Account();
acc.Name = ‘Test Account’;
insert acc;
List opportunities = new List();
for(Integer i=0;i < 10;i++){ opportunities.add(new Opportunity( Name = 'Test Opp', AccountId = acc.Id, StageName = 'Closed Won', CloseDate = Date.today() )); } insert opportunities; Test.startTest(); List opportunitiesFromService = AccountService.getOpportunitiesOnAccount(acc.Id);
Test.stopTest();

System.assertEquals(10, opportunitiesFromService.size(), ‘There are 10 opportunities’);
}

static testMethod void updateAccountName(){
Account acc = new Account();
acc.Name = ‘Test Account’;
insert acc;

Test.startTest();
AccountService.updateAccountName(acc, ‘Updated Account’);
Test.stopTest();

Account accFromDb = [SELECT Id, Name FROM Account WHERE Id = :acc.Id];
System.assertEquals(‘Updated Account’, accFromDb.Name, ‘The account name has been updated’);
}
}
[/java]

So, what’s wrong with the current approach above? Well, you need to generate the same data several times, but it is also done in slightly different pieces of code. This can cause maintainability problems as well as well as a slow process generating the same data. How would this look using the new @testSetup method?

[java]
@isTest
public class AccountServiceTest {
@testSetup static void setupTestData(){
List accounts = new List();
for(Integer i=0;i < 10;i++){ accounts.add(new Account(Name = 'Test Acc')); } insert accounts; List opportunities = new List();
for(Account acc:accounts){
for(Integer i=0;i < 10;i++){ opportunities.add(new Opportunity( Name = 'Test Opp', AccountId = acc.Id, StageName = 'Closed Won', CloseDate = Date.today() )); } } insert opportunities; } static testMethod void testGetAllAccounts(){ Test.startTest(); List accountsFromService = AccountService.getAllAccounts();
Test.stopTest();

System.assertEquals(10, accountsFromService.size(), ‘There should be 10 accounts’);
}

static testMethod void testGetOpportunitiesOnAccount(){
Account acc = [SELECT Id, Name FROM Account WHERE Name = ‘Test Acc’ LIMIT 1];

Test.startTest();
List opportunitiesFromService = AccountService.getOpportunitiesOnAccount(acc.Id);
Test.stopTest();

System.assertEquals(10, opportunitiesFromService.size(), ‘There are 10 opportunities’);
}

static testMethod void updateAccountName(){
Account acc = [SELECT Id, Name FROM Account WHERE Name = ‘Test Acc’ LIMIT 1];

Test.startTest();
AccountService.updateAccountName(acc, ‘Updated Account’);
Test.stopTest();

Account accFromDb = [SELECT Id, Name FROM Account WHERE Id = :acc.Id];
System.assertEquals(‘Updated Account’, accFromDb.Name, ‘The account name has been updated’);
}
}
[/java]

Note how there is only a single method that inserts data and also note that I can directly query records immediately in a test yet @SeeAllData is not set. We are accessing the data that was set up when the class was first instantiated. Now let’s tweak the code just slightly to verify the rollback works properly.

[java]
@isTest
public class AccountServiceTest {
@testSetup static void setupTestData(){
List accounts = new List();
for(Integer i=0;i < 10;i++){ accounts.add(new Account(Name = 'Test Acc')); } insert accounts; List opportunities = new List();
for(Account acc:accounts){
for(Integer i=0;i < 10;i++){ opportunities.add(new Opportunity( Name = 'Test Opp', AccountId = acc.Id, StageName = 'Closed Won', CloseDate = Date.today() )); } } insert opportunities; } static testMethod void testGetAllAccounts(){ Test.startTest(); List accountsFromService = AccountService.getAllAccounts();
Test.stopTest();

System.assertEquals(10, accountsFromService.size(), ‘There should be 10 accounts’);
}

static testMethod void testGetOpportunitiesOnAccount(){
Account acc = [SELECT Id, Name FROM Account WHERE Name = ‘Test Acc’ LIMIT 1];

Test.startTest();
List opportunitiesFromService = AccountService.getOpportunitiesOnAccount(acc.Id);
Test.stopTest();

System.assertEquals(10, opportunitiesFromService.size(), ‘There are 10 opportunities’);
}

static testMethod void updateAccountName(){
Account acc = [SELECT Id, Name FROM Account WHERE Name = ‘Test Acc’ LIMIT 1];

Test.startTest();
AccountService.updateAccountName(acc, ‘Updated Account’);
Test.stopTest();

Account accFromDb = [SELECT Id, Name FROM Account WHERE Id = :acc.Id];
System.assertEquals(‘Updated Account’, accFromDb.Name, ‘The account name has been updated’);
}

static testMethod void testRollbackOnTestData(){
List accounts = [SELECT Id, Name FROM Account WHERE Name = ‘Updated Account’];

System.assertEquals(0, accounts.size(), ‘There are no accounts with that name because they have been rolled back’);
}
}
[/java]

The last test was added to verify that the data rolls back to it’s original state, which it does (otherwise we would find an account with the updated name).

Other Considerations

From the release notes:

  • Test setup methods are supported only with the default data isolation mode for a test class. If the test class or a test method has access to organization data by using the @isTest(SeeAllData=true) annotation, test setup methods aren’t supported in this class. Because data isolation for tests is available for API versions 24.0 and later, test setup methods are also available for those versions only.
  • Multiple test setup methods are allowed in a test class, but the order in which they’re executed by the testing framework isn’t guaranteed.
  • If a fatal error occurs during the execution of a test setup method, such as an exception that’s caused by a DML operation or an assertion failure, the entire test class fails, and no further tests in the class are executed.
  • If a test setup method calls a non-test method of another class, no code coverage is calculated for the non-test method.

My Observations

This is a big step forward to achieving faster test runs. Faster unit test execution time is pivotal to Test Driven Development (TDD). This is a feature that will immediately impact the way everyone develops as every single Salesforce developer will want to utilize this functionality.

The main downside to all of this is the fact that the only way to access this data is to query it back from the database. While this may not be a problem in the simple example above, it may become more of an issue as the complexity grows.

With that said, it is still worth it to use this new functionality. You will still want to incorporate good unit test practices (such as using a TestUtils class), this will just be a new aspect of proper unit test structure. Enjoy and feel free to test it out on your own Spring ’15 Pre-Release Org!

Advertisement

Go to Smartblog Theme Options -> Ad Management to enter your ad code (300x250)

Comments are closed.