Use Mock and Stub Objects
Learning Objectives
- Explain what a mock and a stub are.
- Describe when to use a mock and a stub.
- Write unit tests using mocks and stubs.
The Pattern
Mocks and stubs are more advanced topics in the realm of unit testing. However, they’re incredibly useful for making tests easier to write, understand, and maintain. They also insulate the code you’re testing from changes to other parts of your code base. Check out this video for an introduction to mocks and stubs.
In this unit, we create and use an HttpMock
and a custom stub object. Often collectively referred to as mock objects, they serve the same purpose: they are fake objects that stand in for real instances of objects. Because they’re fake, we can override their functionality and return the data of our choosing.
Technically, a mock and a stub object are slightly different. Mock objects work on the object level. Stubs replace individual methods. On the Lightning Platform, developers write mocks and stubs by extending platform interfaces. For example, to create an HTTP response mock, you create a class that extends the HTTPCalloutMock
interface. (We look at this in more detail in just a moment.) We create mock objects to isolate the code we’re testing from any code in our org that we’re not testing—for example, third-party code, services, and even classes other than the one we’re actively testing.
So what are the use cases for mock and stub objects? There are two classic use cases for mock objects. The first is specialized. You find it whenever you’re making a callout from Apex to a third-party web service. In that case, you need to mock the HTTP callout. The second use case is more generic. You come across this use case whenever you’re testing code that relies on another object’s internal state or implementation. In these situations, it’s incredibly useful to use a stub object while testing.
Let’s see what these two use cases look like from a pattern perspective.
Mock Object Pattern
Here’s the pattern for HttpCalloutMock
.
- Create a class that implements the
HttpCalloutMock
interface. For example,HTTPMockFactory
. - Create or load your test data.
- Create an instance of
HTTPMockFactory
. For example,HTTPMockFactory mockInstance = new HTTPMockFactory()
. - Call
Test.setMock(mockInstance)
, passing in your mock object instance that you created in the previous step. - Call
Test.startTest();
- Execute your code that makes a callout.
- Call
Test.stopTest();
- Make assertions to ensure your code functions as expected.
Stub Object Pattern
When you’re using stub objects, the pattern is similar.
- Create or load your test data.
- Create an instance of a class that implements the
StubProvider
interface. - Call
Test.startTest();
- Call the code you want to test, passing in your stub instance.
- Call
Test.stopTest();
- Make assertions to ensure your code functions as expected.
Create an HTTPMockFactory Class
Let’s look at creating a new HTTPCalloutMock
class and using it in a test context.
- Open VS Code.
- In the Explorer sidebar, right-click the folder
classes
, then choose SFDX: Create Apex Class. - Name the class
HTTPMockFactory
and accept the default directory. - Replace the default contents with the following code.
@IsTest public class HTTPMockFactory implements HttpCalloutMock { protected Integer code; protected String status; protected String body; protected Map<String, String> responseHeaders; public HTTPMockFactory( Integer code, String status, String body, Map<String, String> responseHeaders ) { this.code = code; this.status = status; this.body = body; this.responseHeaders = responseHeaders; } public HTTPResponse respond(HTTPRequest req) { HttpResponse res = new HttpResponse(); for(String key : this.responseHeaders.keySet()) { res.setHeader(key, this.responseHeaders.get(key)); } res.setBody(this.body); res.setStatusCode(this.code); res.setStatus(this.status); return res; } }
- Click File > Save.
- Right-click the file you’re working on, then choose SFDX: Deploy Source To Org.
Code Highlights
This class’s constructor accepts parameters that the respond method passes back. Like our testFactory for data, this factory allows us to define the mock on the fly, as part of our test.
The Test
In the package you installed in unit 1 of this module is a class called ExternalSearch.cls
. It accepts a search string and executes a web search of it for you. Let's write a unit test for it with our mock factory.
- Open VS Code.
- In the Explorer sidebar, right-click the folder
classes
, then choose SFDX: Create Apex Class. - Name the class
ExternalSearchTests
. - Replace its contents with the following code.
@IsTest private class ExternalSearchTests { @IsTest static void testPositiveMocking() { // GIVEN HTTPMockFactory mock = new HTTPMockFactory( 200, 'OK', 'I found it!', new Map<String, String>() ); Test.setMock(HttpCalloutMock.class, mock); // WHEN Test.startTest(); String result = ExternalSearch.googleIt('epic search'); Test.stopTest(); // THEN Assert.areEqual('I found it!', result, 'Expected to receive mock response'); } }
- Click File > Save.
- Right-click the file you’re working on, then choose SFDX: Deploy Source To Org.
- Click the Run Test button that appears on the
testPositiveMocking
method.
Code Highlights
The key to this test is to call Test.setMock()
. This call ensures your code never actually makes a callout, returning the HttpResponse you specify instead. In this case, that means the return value of our googleIt
method is “I found it!” This ability to inject the return value into the mock factory lets you easily write both positive and negative tests.
Stubbing
Stubbing is similar to mocking, but it’s more flexible.
- Open VS Code.
- In the Explorer sidebar, click the folder
classes
. - Select the class OpportunityDiscount. This is the code you’re testing.
The getTotalDiscount()
method determines the total discount for an opportunity based on whether the account is high priority. Because the implementation of isHighPriority()
may change over time, we don’t want to be dependent on how it works internally when we test the getTotalDiscount()
method.
This is the perfect situation to create a stub of the AccountWrapper
class. Let’s write a quick test to demonstrate that.
- Open VS Code.
- In the Explorer sidebar, right-click the folder
classes
, then choose SFDX: Create Apex Class. - Name the class
OpportunityDiscountTests
and accept the default directory. - Replace its contents with the following code.
@IsTest private class OpportunityDiscountTests { @IsTest static void testPositiveStubbingLowPriority() { // GIVEN AccountWrapper mockAccountWrapper = (AccountWrapper) Test.createStub(AccountWrapper.class, new AccountWrapperMock()); OpportunityDiscount od = new OpportunityDiscount(mockAccountWrapper); // WHEN Test.startTest(); Decimal result = od.getTotalDiscount(); Test.stopTest(); // THEN Assert.areEqual(.1, result, 'Expected to get .1'); } @IsTest static void testPositiveStubbingHighPriority() { // GIVEN AccountWrapperMock.isHighPriorityReturn = true; AccountWrapper mockAccountWrapper = (AccountWrapper) Test.createStub( AccountWrapper.class, new AccountWrapperMock() ); OpportunityDiscount od = new OpportunityDiscount(mockAccountWrapper); // WHEN Test.startTest(); Decimal result = od.getTotalDiscount(); Test.stopTest(); // THEN Assert.areEqual(.25, result, 'Expected to get .25'); } }
- Click File > Save.
- Right-click the file you’re working on, then choose SFDX: Deploy Source To Org.
- Click the Run All Tests button that appears at the top of the class.
Code Highlights
These two test methods are almost identical. Structurally, the tests work by creating a mock instance of AccountWrapper
. That account wrapper is injected into the OpportunityDiscount
object in place of an actual AccountWrapper
object. As a result, we know exactly how the AccountWrapper
object responds when the isHighPriority()
method is called.
The key difference is on the first line of the second test method, where you set a static variable on the AccountWrapperMock
. This is a neat trick that’s useful for manipulating the behavior of your zero-parameter stubbed methods. When the methods you’re stubbing do not accept parameters, you can use a static variable defined in your stub class to intentionally alter the behavior of the stub in multiple tests.
Conclusion
This module is jam-packed with information. We’ve covered why we test, positive and negative testing, permissions testing, mocking and stubbing, and testing Lightning components. Together, these patterns and tools establish tests that are a productive part of the code base. They also can help you become a better developer, since writing tests start to shape how you write your code. Remember that the best way to internalize these ideas and practices is to write more tests!