Skip to content

lmeerkatz/df16-apex-testing

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 

Repository files navigation

Introduction to Apex Testing - Dreamforce 2016

Presented by Laura Meerkatz and Adam Lincoln, developers at Salesforce.org

This readme provides resources related to that session. This repository should also contain everything you need to deploy this code to a dev org.

Why we write tests

Short answer? Because we like to sleep at night.

Longer answer:

  • During development, tests show us where our architectural plan may be wrong.
  • At initial release, tests give us proof that our new code does what we want.
  • When we update existing code, tests give us confidence that changes to our code do not break existing functionality.
  • Test runs during deployment warn us that new code is trying to break existing code (and prevents that code from deploying).
  • Running tests in production can tell us when a configuration change has broken our code.

Testing Basics

Test Class Structure

Sample Test Class

@isTest
private class SampleTest {

@TestSetup 
static void setup(){
	// insert sample data that you want for all test methods here
}
    
// use comments to describe the scenario you're testing
@isTest
static void testSomething(){
	// set up test data for this scenario
        
	// execute the logic you're testing
        
	// query for the updated record(s)
        
	// assert expected results
	}
}

What to Test

  • Positive tests (things that should happen do happen)
  • Negative tests (things that shouldn't happen don't happen)
  • Exception tests (exceptions we're expecting are thrown as expected)
  • Bulk tests (everything still works when we're dealing with lots of records)

Sample Scenario

We have code to calculate employee bonuses. Employees should earn a 1% bonus for all Closed Won opportunities this year. The maximum bonus is $25,000. If an employees total opp amount is negative, an exception is thrown.

What should we test?

Things that should happen:

  • Employees with closed won opportunities should get a bonus based on the amount
  • Employees with lots of closed won opps should receive the maximum bonus

Things that shouldn't happen:

  • Employees who don't have closed opps should not get a bonus
  • Open opps shouldn't count toward the bonus amount

Exception testing:

  • A negative total opp amount should result in an exception

Bulk testing:

  • Calculate bonus for an employee with 200 closed opps

Here's what that looks like (full code):

Employees with closed won opportunities should get a bonus based on the amount

// test employee with some open opps and some closed opps
// they should get a bonus
@isTest 
static void testAwardBonus() {
	// set up data
	User employee = TestData.standardUser;
	
        List opps = TestData.createOpportunities(testAccount, 3);
    	opps[0].Amount = 1000;
    	opps[0].StageName = 'Closed Won';
    	opps[1].Amount = 10000;
    	opps[1].StageName = 'Prospecting';
    
	opps[2].Amount = 100000;
    	opps[2].StageName = 'Closed Won';
    
        insert opps;
	
	// execute the logic we're testing
	EmployeeBonusManager.updateEmployeeBonuses(employee.Id);
        
        // query for updated record
        employee = queryForUser(employee.Id);
        
        // assert expected results
        System.assertEquals(1010, employee.Bonus__c, 'Employee has have bonus for $101,000 in opps');
}

Employees with lots of closed won opps should receive the maximum bonus

// test employee who should get the maximum bonus
static void testAwardMaximumBonus() {
	// set up data
	User employee = TestData.standardUser;
		
        List opps = TestData.createOpportunities(testAccount, 1);
    	opps[0].Amount = 60000000;
    	opps[0].StageName = 'Closed Won';
    
        insert opps;
	
	// execute the logic we're testing
	EmployeeBonusManager.updateEmployeeBonuses(employee.Id);
        
        // query for updated record
        employee = queryForUser(employee.Id);
        
        // assert expected results
	System.assertEquals(25000, employee.Bonus__c, 'Employee should be awarded the maximum bonus');
}

Employees who don't have closed opps should not get a bonus

// test employee with no opps
// they shouldn't get a bonus
@isTest 
static void testNoBonusNoOpps(){
	// set up data
	User employee = TestData.standardUser;
	
	// execute the logic we're testing
	EmployeeBonusManager.updateEmployeeBonuses(employee.Id);
        
	// query for updated record
	employee = queryForUser(employee.Id);
        
	// assert expected results
	System.assertEquals(null, employee.Bonus__c, 'Employee has no opportunities and should have no bonus');
}

Open opps shouldn't count toward the bonus amount

// test employee with only open opps
// they shouldn't get a bonus
@isTest 
static void testNoBonusOnlyOpenOpps(){
	// set up data
	User employee = TestData.standardUser;
		
        List opps = TestData.createOpportunities(testAccount, 3);
        for (Opportunity opp : opps) {
		opp.StageName = 'Prospecting';
        }
        insert opps;

	// execute the logic we're testing
	EmployeeBonusManager.updateEmployeeBonuses(employee.Id);
        
        // query for updated record
        employee = queryForUser(employee.Id);
        
        // assert expected results
        System.assertEquals(null, employee.Bonus__c, 'Employee has only open opportunities and should have no bonus');
}

A negative total opp amount should result in an exception

// test negative total opp amount
// this should throw an exception
@isTest 
static void testNegativeOppTotal(){
	// set up data
        User employee = TestData.standardUser;
        List opps = TestData.createOpportunities(testAccount, 3);
        for (Opportunity opp : opps) {
        	opp.StageName = 'Closed Won';
		opp.Amount = -5;
        }
        insert opps;
        
       	Boolean exceptionThrown = false;
        
        try {
		// execute the logic we're testing
		EmployeeBonusManager.updateEmployeeBonuses(employee.Id);
        } catch (Exception ex) {
		exceptionThrown = true;
		System.assert(ex instanceOf EmployeeBonusManager.BonusException, 'Thrown exception should be a Bonus Exception');
        }
                
        // assert expected results
        System.assert(exceptionThrown, 'An exception should have been thrown');
    }

Calculate bonus for an employee with 200 closed opps

// test employee bonus with several opps
@isTest 
static void testBonusBulk(){
	// set up data
	User employee = TestData.standardUser;
        List opps = TestData.createOpportunities(testAccount, 200);
        
        for (Opportunity opp : opps) {
		opp.Amount = 10000;
		opp.StageName = 'Closed Won';
        }
        insert opps;
        
        // execute the logic we're testing
        EmployeeBonusManager.updateEmployeeBonuses(employee.Id);
        
        // query for updated record
        employee = queryForUser(employee.Id);
        
        // assert expected results
        System.assertEquals(25000, employee.Bonus__c, 'Employee should be awarded the maximum bonus');
}

Best Practices

Create your own data

By default, your tests don't have access to data in your org. That's a good thing!

  • Isolating makes writing assertions easier. (You can do things like query for a count of all records and know that you're only getting back results you created in your test.)
  • It prevents row-lock errors. (If your tests are updating a record from your real dataset and a real user tries to update that record at the same time, your user can get locked out of making changes.)

You can override that behavior by adding the ([SeeAllData=true] (https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_testing_seealldata_using.htm)) annotation to your test class or method. There are a few cases where this is necessary, but as a general rule you should avoid it.

*Note: There are a few objects like User that are available to tests regardless of whether SeeAllData is set. Changes made to these records in tests are not persisted outside of tests.

Use test data factories

A test data factory is a class that makes it easy to create several records quickly, so you don't have to spend as much time setting up data for your tests.

@isTest 
public class TestData {
	public static List createAccounts(Integer count) {
		List accts = new List();
        	for (Integer i = 0; i < count; i++) {
            		// at a minimum, add enough data to pass validation rules
			accts.add(new Account(
				Name = 'Test Account ' + i
			));
        	}
        return accts;
}
    
public static List createContacts(Account acct, Integer count) {
	List cons = new List();
	for (Integer i = 0; i < count; i++) {
        	cons.add(new Contact(
			AccountId = acct.Id,
            		FirstName = 'Joe',
            		LastName = 'McTest ' + i
            	));
        }
        return cons;
    }
    ...
}

You can also store test data as a static resource in a .csv file and load the records using Test.loadData().

Use @TestSetup to create data for your test class in one step

You can have a single method in each test class annotated with @TestSetup. This method will run once before any test methods run, and at the end of each test the data will be rolled back to its state before the test. Using @TestSetup makes writing your tests faster and it makes them run faster.

@TestSetup 
static void setup(){
	Account testAccount = TestData.createAccounts(1)[0];
        testAccount.Name = 'Apex Testing DF16 Co.';
        insert testAccount;
}

Use System.runAs() to test user access

In a test, you can execute specific blocks of code as a certain user, which means that you can use tests to verify that a user can do the things they should be able to do, and can't do the things they should be blocked from doing.

@isTest
static void testPrivilegedUser(){
Boolean exceptionCaught;
System.runAs(TestData.adminUser){
try {
SomeClass.doDangerousOperation();
} catch(Exception e) {
exceptionCaught = true;
}
}
System.assertEquals(false, exceptionCaught, 'Admin should be able to execute doDangerousOperation');
}

@isTest
static void testLimitedUser(){
Boolean exceptionCaught;
System.runAs(TestData.standardUser){
try {
SomeClass.doDangerousOperation();
} catch(Exception e) {
exceptionCaught = true;
}
}
System.assertEquals(true, exceptionCaught, 'Standard user should NOT be able to execute doDangerousOperation');
}

Special Cases

Visualforce Controllers and Extensions

You can and should test the logic behind your Visualforce pages. Any action that is called from your controller can be tested in an Apex test. Actions in the page UI itself (including anything involving JavaScript) can be covered in end-to-end tests, but that is outside of Apex testing (and not covered here).

Here's how to set up a test for a custom controller:

// set the current page
PageReference pageRef = Page.EmployeeBonuses;
Test.setCurrentPage(pageRef);

// set up the controller
EmployeeBonusController ctrl = new EmployeeBonusController();

// call method(s) in the controller
ctrl.doInit();

// check the resulting data by referencing the property in the class
List employees = ctrl.employees;    

// assert expectations 
System.assertEquals(2, ctrl.employees.size(), 'The list should have two employees');
System.assertEquals(0, ApexPages.getMessages().size(), 'There should be no error messages on the page');

Extensions are exactly the same, with an additional step to set up the standard controller and pass it to the extension:

// set the current page
PageReference pageRef = Page.EmployeeBonuses;
Test.setCurrentPage(pageRef);

// set up the standard controller    
ApexPages.StandardController standardCtrl = new ApexPages.StandardController(new Opportunity());

// set up the extension, referencing the standard controller
EmployeeBonusExtension extension = new EmployeeBonusExtension(standardCtrl);

// call method(s) in the extension
extension.doInit();

// check the resulting data by referencing the property in the class
List employees = extension.employees;    

// assert expectations 
System.assertEquals(2, extension.employees.size(), 'The list should have two employees');
System.assertEquals(0, ApexPages.getMessages().size(), 'There should be no error messages on the page');

You can see a full code sample here.

Lightning Component Controllers

Lightning Component controllers are similar, but because all @AuraEnabled methods are static, you don't have to initialize the controller class. You also don't check for error messages from the controller because all error handling for Lightning Components is done on the client side.

// call the @AuraEnabled method
List<User> employees = EmployeeBonusController.getEmployeeList();

// assert that you get the expected results
System.assertEquals(2, employees.size(), 'The list should have two employees');

You can see a full code sample here.

Callouts

You can't do a callout from a test, but you can fake it. First, generate the response that you want to return by implementing HttpCalloutMock:

@isTest
public class EmployeeBonusCompareMock implements HttpCalloutMock {
	public HttpResponse respond(HttpRequest req) {
		// Look at req.getMethod(), req.getEndpoint() to decide what to return
		HttpResponse resp = new HttpResponse();
        	resp.setHeader('Content-Type', 'application/json');
        	resp.setBody('{"id": 10000, "industry_average": 0.30}');
        	resp.setStatusCode(200);
        	return resp;
    	}
}

Then set the mock response in your test by using Test.setMock():

@isTest
static void testCallout() {
Test.setMock(HttpCalloutMock.class, new EmployeeBonusCompareMock());
User testUser = TestData.standardUser;

Test.startTest();
Id qJobId = System.enqueueJob(new EmployeeBonusCompare(testUser.Id));
// All asynchronous work runs synchronously when Test.stopTest() is called
Test.stopTest();

// Requery to get newly computed Bonus_Compared_to_Industry__c value
testUser = [SELECT Bonus_Compared_to_Industry__c FROM User WHERE Id = :testUser.Id];
System.assertNotEquals(null, testUser.Bonus_Compared_to_Industry__c);

}

You can see a full code sample here.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published