Coder Social home page Coder Social logo

df16-apex-testing's Introduction

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.

df16-apex-testing's People

Contributors

lmeerkatz avatar

Stargazers

Eugene Voronko avatar Marina Borges avatar Sanja avatar Ryan Riehle avatar Liz Kao avatar

Watchers

Adam Lincoln avatar Liz Kao avatar Sanja avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.