Sunday, September 15, 2013

Unit Testing Tips & Tricks - Part 1

In my experience, unit testing does more to assure the quality of software than any other practice. If you care about the quality of your work, you must become proficient at writing unit tests. This is my first in a series of posts about unit testing.

Tip #0 Write Real Unit Tests

Salesforce requires that a certain percentage of your code is covered by unit tests. Too often, this requirement is "met" by throwing in meaningless unit tests after development is complete - just before deployment.
@isTest
public class My_Bare_Minimum_Tests {
  static testMethod void testMyController() {
    Test.startTest();
      MyController c = new MyController();
      String s = c.someProperty;
      c.foo();
      c.bar();
    Test.stopTest();
  }
}

This isn't effective unit testing and won't do much for ensuring the quality or correctness of your code. Do better. Find the information you need and develop the skill of writing great unit tests. Here are some excellent resources if you need some motivation:

Tip #1 Maintain 100% Code Coverage

If you're new to unit testing, attaining 100% coverage may seem impossible. As with most things in life, though, practice makes easy. You'll quickly find yourself either writing tests before your code, writing code & tests at the same time, or designing your code so that it's 100% testable.
  • Don't wait until the end of the project to start unit testing. At that point, it's too much work and too risky to refactor code so that it's 100% testable.
  • Design your code with testing in mind. Move all code to testable classes, instead of leaving it in tougher-to-access constructs such as triggers, batch processes, and web services.
  • Don't hide errors. A tempting error handling (avoidance?) move is to place code in a try block with an empty catch. Don't do it. I'll provide some alternatives in a later post.

I've heard it argued that there's a point of diminishing returns with unit tests. It's not worth getting those last few lines covered. I disagree. If you have anything less than 100% coverage, it's very difficult to know when untested code has been added. And then... what's too low? 98% - "well, that's really close to 100." 91% - "well, I'm still much higher than most SF code." 78% - "well, I've still got enough coverage to deploy." You get the picture. When the schedule gets tight, it's easy to rationalize letting your unit testing slip. That's harder to do if you're committed to maintaining 100% coverage.

Tip #2 Create Test Utilities

So now that we're committed, let's make this as easy as possible.

A class designated as @isTest can provide public utility properties and methods for your arsenal of unit tests. This will speed up your test development and make your tests more maintainable. Some things I like to include in this class are a common initialization method, User instances to be used with System.runAs(), and SObject creation methods. The following snippet is the shell of a utilities class. We'll fill in the methods and properties later.

/**
 * A collection of utilities and methods for use in unit tests.
 * @author Steve Cox
 */
@isTest
public class SF_Test {
  //--------------------------------------------------------------------------
  // Properties
  public static User testRunner {...}


  //--------------------------------------------------------------------------
  // Methods
  public static void init() {...}

  public static Account newAccount() {...}
}

Tip #3 Centralize Initialization Code

I find that unit tests often require some common initialization. For example, maybe much of your code relies on a custom setting, or the existence of a certain user or profile. So the first addition to our utilities class will be an init() method.

/**
 * global unit test initialization
 * Include creation and initialization of required custom settings, 
 * expected Accounts, Users, Profiles, etc.
 */
public static void init() {
  // create our global settings object and set the defaults
  My_Settings__c settings = new My_Settings__c();
  settings.Page_Size__c = 5;
  settings.debugging__c = true;
  insert settings;
}

Now we can call this from each of our test methods and safely assume our settings are valid.

static testMethod void testSomething() {
  SF_Test.init();
  
  Test.startTest();
    ...
    if (My_Settings__c.getInstance().debugging__c) {
       ...
    }
    ...
  Test.stopTest();
}

Tip #4 RunAs a Known User

Tests can be run by you, by another developer, or by anyone else logged into your org with the appropriate permissions. In order to create a 'controlled environment' for your tests, it's best practice to always runAs a certain user. Perhaps your code is part of a portal or community app. In that case, you should runAs a portal user. Maybe your code is designed to work differently depending on the profile of the logged-in user. Testing those differences will require that you runAs various users. You probably don't want to duplicate the code to create these users in several tests, so this is another good candidate for our test utilities class. Creating the user as a property makes it handy to access, and will not be executed until invoked - keeping your tests speedy.

/** a default user to use in System.runAs() */
public static User testRunner {
  get {
    if (null == testRunner) {
      // all test code should execute under a user we can control so as to avoid
      // surprises when deploying to different environments.
      UserRole[] roles = [SELECT Id FROM UserRole WHERE DeveloperName = 'Admin'];
      if (SF.isEmpty(roles)) {
        roles.add(new UserRole(DeveloperName = 'Admin', Name = 'r0'));
        insert roles;
      }
      
      testRunner = newUser('runner@test.com');
      testRunner.UserRoleId = roles[0].Id;
      insert testRunner;
    }

    return testRunner;
  }

  private set;
}

Again, applying this to our unit tests saves us lots of time and avoids the plague of code duplication:

static testMethod void testSomething() {
  System.runAs(SF_Test.testRunner) {
    SF_Test.init();

    Test.startTest();
      ...
    Test.stopTest();
  }
}

Tip #5 Add SObject Creation Utilities

Tests often need to create Accounts, Contacts, or instances of your custom SObjects. Each of these has various business requirements - required fields, valid formats, etc. Those requirements might change over the life of your org. The code required for creating an object and satisfying the business requirements may get bulky. Providing a single, centralized method for creating one of these objects makes your test code much more maintainable.

/** create a valid customer account object */
public static Account newCustomerAccount(String name) {
  return new Account(
    RecordTypeId = SF_Rt.getId(Account.SObjectType, SF_Rt.Name.Customer_Account),
    Name = name,
    Sector__c = 'Medical', // required field
    Tax_Id__c = '123456789' // another required field
  }
}

Now we can quickly create customer accounts in our unit tests. If the business requirements for a valid customer account change, we only need to change our utility method -- not all of our unit tests that use customer accounts.

static testMethod void testSomething() {
  System.runAs(SF_Test.testRunner) {
    SF_Test.init();

    Account myCustomerAccount = SF_Test.newCustomerAccount('customer 1');
    insert myCustomerAccount;

    Test.startTest();
      ...
    Test.stopTest();
  }
}

Until next time, have fun writing great unit tests!

No comments:

Post a Comment