Skip to main content

Use Mock and Stub Objects

Learning Objectives

After completing this unit, you’ll be able to:

  • Explain what a mock and a stub are.
  • Describe when to use a mock and a stub.
  • Write unit tests using mocks and stubs.

Understand Mock and Stub Objects

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 Salesforce 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.

Mock and Stub Testing Patterns

Let’s see what these two use cases look like from a pattern perspective.

Mock Object Pattern

Here’s the pattern for HttpCalloutMock.

  1. Create a class that implements the HttpCalloutMock interface. For example, HTTPMockFactory.
  2. Create or load your test data.
  3. Create an instance of HTTPMockFactory. For example, HTTPMockFactory mockInstance = new HTTPMockFactory().
  4. Call Test.setMock(mockInstance), passing in your mock object instance that you created in the previous step.
  5. Call Test.startTest();
  6. Execute your code that makes a callout.
  7. Call Test.stopTest();
  8. Make assertions to ensure your code functions as expected.

Stub Object Pattern

When you’re using stub objects, the pattern is similar.

  1. Create or load your test data.
  2. Create an instance of a class that implements the StubProvider interface.
  3. Call Test.startTest();
  4. Call the code you want to test, passing in your stub instance.
  5. Call Test.stopTest();
  6. 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.

  1. Open VS Code.
  2. In the Explorer sidebar, right-click the folder classes, then choose SFDX: Create Apex Class.
  3. Name the class HTTPMockFactory and accept the default directory.
  4. 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 response = new HttpResponse();
        for(String key : this.responseHeaders.keySet()) {
          response.setHeader(key, this.responseHeaders.get(key));
        }
        response.setBody(this.body);
        response.setStatusCode(this.code);
        response.setStatus(this.status);
        return response;
      }
    }
  5. Click File | Save.
  6. 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 is a class called ExternalSearch.cls. It accepts a search string and executes a web search for it.

public class ExternalSearch {
  public class ExternalSearchException extends Exception {
  }


  public static string googleIt(String query) {
    Http h = new Http();
    HttpRequest req = new HttpRequest();
    req.setEndpoint('https://www.google.com?q=' + query);
    req.setMethod('GET');
    HttpResponse res = h.send(req);
    if (res.getStatusCode() != 200) {
      throw new ExternalSearchException(
        'Did not receive a 200 status code: ' + res.getStatusCode()
      );
    }
    return res.getBody();
  }
}

Now, let’s write a unit test for it with our mock factory.

  1. Open VS Code.
  2. In the Explorer sidebar, right-click the folder classes, then choose SFDX: Create Apex Class.
  3. Name the class ExternalSearchTests.
  4. 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');
      }
    }
  5. Save the file and deploy it to your org.
  6. 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. In the package you installed is a class called OpportunityDiscount.cls. Let’s take a look.

public class OpportunityDiscount {
  AccountWrapper acct;


  public OpportunityDiscount(AccountWrapper acct) {
    this.acct = acct;
  }


  public Decimal getTotalDiscount() {
    if (this.acct.isHighPriority()) {
      return .25;
    } else {
      return .1;
    }
  }
}

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 theAccountWrapper class. Let’s write a quick test to show how it works.

  1. Open VS Code.
  2. In the Explorer sidebar, right-click the folder classes, then choose SFDX: Create Apex Class.
  3. Name the class OpportunityDiscountTests and accept the default directory.
  4. 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 oppDiscount = new OpportunityDiscount(mockAccountWrapper);
        // WHEN
        Test.startTest();
          Decimal result = oppDiscount.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 oppDiscount = new OpportunityDiscount(mockAccountWrapper);
        // WHEN
        Test.startTest();
          Decimal result = oppDiscount.getTotalDiscount();
        Test.stopTest();
        // THEN
        Assert.areEqual(.25, result, 'Expected to get .25');
      }
    }
  5. Click File | Save.
  6. Right-click the file you’re working on, then choose SFDX: Deploy Source To Org.
  7. 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. You learned about positive and negative testing, permissions testing, and mocking and stubbing. Together, these patterns and tools establish tests that are a productive part of the code base. They can also 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!

Partagez vos commentaires sur Trailhead dans l'aide Salesforce.

Nous aimerions connaître votre expérience avec Trailhead. Vous pouvez désormais accéder au nouveau formulaire de commentaires à tout moment depuis le site d'aide Salesforce.

En savoir plus Continuer à partager vos commentaires