[SalesForce] What are the advantage of the @testSetUp annotation

Before Salesforce introduced @testSetup annotation in 2015, I used to create common data in a method and call it in every test method.

Ex:

@isTest
private class clsAccountTest {

    private static Account testAccount;

    private static void createCommonData(){
        testAccount = new Account(Name= 'Avinash', BillingPostalCode = '97214');
        insert testAccount;
    }

    @isTest static void itShouldAskForZipCode(){
        createCommonData();
        //testAccount is accessible.
        testAccount.BillingPostalCode = '12345';
        update testAccount;
    }
}

Now, with @testSetUp, it would look like:

@isTest
private class clsAccountTest {

    private static Account testAccount;

    @testSetup static void commonData() {
        testAccount = new Account(Name= 'Avinash', BillingPostalCode = '97214');
        insert testAccount;
        System.Debug('****SOQL Queries issued****'+limits.getQueries()); //returns 5
        System.Debug('****DML Statements issued****'+limits.getDMLStatements()); // returns 4. Issued 1 in testSetUp and it caused 3 more in Account trigger.
    }

    @isTest static void itShouldAskForZipCode(){
        System.Debug('****SOQL Queries issued****'+limits.getQueries()); //return 5
        System.Debug('****DML Statements issued****'+limits.getDMLStatements());    //return 4  
        createCommonData();
        //testAccount is not avaialble, so you have to query for it.
        testAccount = [SELECT Id, Name FROM Account WHERE Name = 'Avinash' LIMIT 1];

        testAccount.BillingPostalCode = '12345';
        update testAccount;
    }
}

What do we gain by using this other than less execution time? The Number of SOQL statements, DML statements etc., generated in the testSetUp method are assigned to each and every test method. In fact, it forces us to use more queries as the static variables are not accessible in test methods.

In this answer, the user said that DML statements issued within the testSetUp method are not counted against governor limits, but the DML statements caused by it in the triggers will be counted. I tested this by inserting a record of an object that doesn't have any trigger code and that statement is not correct. Even the DML statements issued within the testSetUp method are counted against governor limits of each and every test method.

I'm wondering if there are any advantages other than less execution time.

Best Answer

To be honest, the increase in speed is about the only tangible benefit to us (as developers).

Salesforce themselves arguably sees more benefit from people using this annotation than we developers (directly) do. If it's faster for us, that means that it also places less load on Salesforce's pods.

As others (including yourself) have noted, it doesn't save on DML or SOQL compared to having a non-annotated static setup method. As another negative (noted by @KeithC), using an @testSetup annotated method means that you don't even have access to the IDs of the records that you're creating. This ends up causing us to use more SOQL queries for each test.

The other benefits that @SebastianKessel pointed out are good points (separating test setup from the test proper, re-usability), but we'd get the same benefit from just having a separate static setup method (without the annotation).

I continue to use @testSetup because, while the benefits to me may not be very noticeable at times, it doesn't really negatively impact me to use it.

I'll end with the pattern that I use to work around the fact that we can't get at the ids of records that are created in an @testSetup method without querying.

+edit: To be clear, this pattern does still perform extra query/queries that wouldn't be needed if we weren't using @testSetup. There isn't any way around that. The point is to provide useful information (record Ids) to individual tests in a central method using as few queries as possible.

@isTest
public class TestSomeClass{
    // My current preference is to have one map per object that I want the Ids of.
    // You really only need one map to hold ids though, even if you have multiple objects.
    private static Map<String, Id> myObjectIdsMap;

    @testSetup
    public static setupEnvironment(){
        // Your standard setup method, aggregating things in collections to keep
        //   various limits (DML and SOQL, mostly) under control
        List<MyObject__c> testRecs = new List<MyObject__c>();

        testRecs.add(new MyObject__c(/*initial state set in here*/));

        insert testRecs;
    }

    private static void setup(){
        // Initializing my map(s) here is mostly just a habit.
        // Should be fine to initialize on the same line it was initially declared on.
        myObjectIdsMap = new Map<String, Id>();

        // While zero-indexed arrays have been burned into my mind as a programmer, 
        //   one-indexed labeling feels better to me for what we're about to do
        Integer i = 1;

        // For every object that we want to grab the Ids from, we need to run a loop.
        // The integer 'i' needs to be reset after each loop.
        // Ordering by Id ASC doesn't guarantee that we'll get the records back in
        //   the same order that they were inserted in, but it seems to do well enough
        //   for unit tests (in practice).
        // If you have another field you can use for ordering, use it (also, don't rely on 
        //   Id ordering to be correct in non-test code).
        for(MyObject__c myObj :[SELECT Id FROM MyObject__c ORDER BY Id ASC]){

            // Using <object label> + <integer> + 'Id' as a convention here
            //   because it's simple and easy to remember.
            // Prefixing the key with the object name is why we only really need one map.
            myObjectIdsMap.put('myObject' + i + 'Id', myObj.Id);
            i++;
        }
    }

    public testMethod void myTest(){
        // The non-annotated setup() method needs to be called in every test method
        // Yes, this removes one of the minor benefits (not needing to explicitly call
        //   a setup method) of using @testSetup, but keeping the common
        //   'auxiliary' setup contained to its own method is better practice
        //   than repeating queries in each test method.
        setup();

        // If you know the value that you want to update for your test, you don't 
        // need to use a query to get the record to update.
        // Just use the sObject constructor, which allows you to set the Id
        MyObject__c myRec = new MyObject__c(
          Id = myObjectIdsMap.get('myObject1Id'),
          some_field__c = 'updated value'
        );

        Test.startTest();
        update myRec;
        Test.stopTest();

        // assertions as appropriate
        // You'll probably need to query the record at this point, but that would
        //   likely need to be done even if you weren't using an @testSetup method
    }
}
Related Topic