October 8, 2010

Apex Test data isolation from Org/User’s data.. !

The key to write a good apex test case is isolate test-data from actual org/user data. If your test case in anyway depends on org data, then it will fail for sure in deployments across different Salesforce orgs.

Unfortunately Apex tests run in SYSTEM mode (The permissions and record sharing of the current user are not taken into account), so their is no isolation from the org/user’s data.

How to Isolate test-data from org’s real data ?

  • All your apex code should declare classes as “with sharing”, unless you really want to access all of the org data. This is really important to ensure sharing rules work correct. In case you need to access all org data i.e. by pass sharing rules, then I suggest creating a new top level or nested class with “without sharing” keywords before class name. This without sharing class should be used to fire all such SOQLs, that need to by pass sharing.
  • Using System.runAs(User) one can create some isolation, this is explained in detail here . But, System.runAs() is effective only if :
    • All Sobjects used in Test case are having “Private” sharing access. But that is not possible and real scenario.
    • Test User profile has no profile overrides on sharing rules, like “Modify All Data” or “Modify/View All” permissions for Sobjects used in Test cases.

So, its pretty hard to ensure that test cases will go good all the times. They will certainly fail for bad combination of sharing rules, profile permissions and org data.

So, how to write stable Apex code + Apex Tests ?

The only good way I found is

“you non-test apex code(trigger/controller) should handle test execution smartly”

This means, one has to write the trigger/controller code to filter records more specifically for current user, (if possible) when running in test mode. One can do this by just doing things

  1. All test code should use System.runAs(<Mock User>) for creating quality test data. Note, here creating a mock user, who doesn’t already exists in system is important for isolation. As if you use some existing user, you might be able to see records owned by him/her. How to do so is explained in this post.
  2. Adding this criteria to all SOQL calls “WHERE OwnerId =:UserInfo.getUserId()” when Controller/Trigger is executed from Test Context. When this criteria is in place, only the test data will be visible to trigger/controller code.

Next, in code samples below, we will try to show how one can implement these above two points in Apex.

Code Sample

This the sample code fixture consists of:

  • A simple custom SObject named “TestObj__c” with “Private” Org wide sharing access. This Sobject is just having a Text field named “SomeTextField”.
  • An apex class named “MyClass”, it can be related to any thing i.e. trigger or a visualforce controller. This class
    • tries to query custom object TestObj__c for a criteria.
    • has configuration attributes to tell, if the class is executing in test context.
  • An apex test class that is written for MyClass. It
    • uses System.runAs etc to ensure tests run correctly in all orgs.
    • creates some TestObj__c records, for testing the MyClass’s query.

MyClass Code

Please note this class’s attributes like isRunningTest, this attribute is required until Salesforce winter’11 release is GA, as winter’11 will give System.isRunningTest() method, that can be used anywhere in test code to know if the code is running in test context. 

public with sharing class MyClass {
    public TestObj__c [] objs {get;set;}
   // Should be set to true, by Apex test cases only
   public boolean isRunningTest = false;    
    private Id contextUserId = Userinfo.getUserId();
       
    public void init() {
       String soql = 'select Id, SomeTextField__c from TestObj__c where SomeTextField__c like \'Gupta\'';
        objs = Database.query(prepareTestSOQL (soql));
    }    
    
    private String prepareTestSOQL (String soql) {
       // after winter'11 we can use System.isRunningTest here
       if (isRunningTest)
         return soql.replaceAll('where', 'where OwnerId =:contextUserId and ');
      else
         return soql;       
    }
}

TestMyClass Code

@isTest
private class TestMyClass{
    public static testMethod void testInit() {
       // Query Standard User or what ever profile best for Test Case
        Profile p = [SELECT Id FROM profile WHERE name='Standard User'];
        // Create a in-memory mock user for running tests in limited data access
        // context
        User mockUser = new User(alias = 'newUser', email='newuser@testorg.com',
        emailencodingkey='UTF-8', lastname='Testing', 
        languagelocalekey='en_US', localesidkey='en_US', profileid = p.Id,
        timezonesidkey='America/Los_Angeles', username='newuser@testorg.com');
      
        System.runAs(mockUser) {    
          // Create 2 test records
          TestObj__c c1 = new TestObj__c (SomeTextField__c='Gupta');
          TestObj__c c2 = new TestObj__c (SomeTextField__c='Gupta');
          insert new TestObj__c[]{c1, c2};
          
          MyClass mycls = new MyClass();
          mycls.isRunningTest = true;
          mycls.init();
        // Now this assertion, should never fail
        // as we are restricting the access in main
        // apex code using Owner Id
          System.assertEquals(2, mycls.objs.size());
        }        
    }
}

Do we have a better way to write Apex Tests ?

I know the above approach and code sample is messy and looks wierd, but this is what I can figure out to make sure that test cases run without crashing across the orgs and multiple deployments. I am sure its not possible to do such owner id replacements always, this can be because of many reasons like

  • Project is too big and has many complex SOQLs.
  • For some reasons we can’t add the ownerid filter in SOQL where clause.

So, if you have any better ideas and ways to crack this problem. Please share with me.

New Idea - Apex tests shouldn’t get access to any org’s data !

So is their a way, really to isolate test cases from Salesforce Org data ? I think no body needs to access org’s user data in apex test cases. Test cases should always run in isolation and their should be no way for them to access what’s the user data. This is because, a single force.com app can be installed in many salesforce orgs, if the test cases rely on org data then they will for sure fail any where because of too much or too less data.

I have posted new idea on Idea Exchange, so that this isolation can be given by salesforce to us in coming releases. If you feel, I am correct and this makes some sense. Then please promote this idea.