Skip to main content
Dreamforce is streaming for free on Salesforce+. Watch now.

Write Full Coverage Tests

Learning Objectives

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

  • Explain why positive tests are important.
  • Write positive unit tests.
  • Explain what negative unit tests are.
  • Write negative unit tests.

Before You Start

This badge is part of a series on Apex unit testing consisting of the following badges.

The content in each badge builds on each other so make sure to take the badges in order.

You also need to install Visual Studio Code and complete the Quick Start: Visual Studio Code for Salesforce Development badge in order to complete the hands-on challenges in this badge.

Write Positive Tests

Positive unit tests are tests that return expected results when you present valid data to the tested code. Think of the classic calculator. A positive test for an addition method might execute with data points 2 and 3 while asserting the output is 5. This is a simplistic example, of course, but the idea is the same. When you execute it with valid data, the code you’re testing should respond with predictable results.

Positive Test Pattern

Writing positive unit tests is all about seeing a set of expected results when you know your input data is good. The standard model for positive tests uses the following steps.

  1. Generate or load test data.
    • Use a data factory or a CSV file.
    • Make assertions to validate your data.
  2. Call Test.startTest().
    • This resets governor limits and isolates your test from test setup.
  3. Execute the code you’re testing.
  4. Call Test.stopTest().
    • This forces any asynchronous code to complete.
  5. Make assertions about the output of your tested code. You might assert a record exists, or that a field is set to an expected value.

To illustrate these steps, we can look at the AccountWrapper class in VS Code. AccountWrapper class is a custom logic layer around the Account object. This is a fairly common way to add custom logic to standard objects. In this case, you’re adding some account-level logic to calculate a rounded average amount of opportunities associated with a given account.

Install VS Code and Connect to Your Trailhead Playground

To execute the tests that you’ll write in this module, you’ll use VS Code. Complete Apex Testing: Prepare for Unit Testing to get VS Code and your Playground set up with the needed code. You’ll use the VSCodeQuickstart project and VS Code to complete this badge.

Install an Unmanaged Package

Install an unmanaged package in your Trailhead Playground that contains code classes that you can write tests for.

For help installing a package in your Trailhead Playground, check out Install a Package or App to Complete a Trailhead Challenge on Salesforce Help.

Package ID: 04taj00000005pJ

In the package you installed is a class called AccountWrapper.cls. Let’s look at it here.

public with sharing class AccountWrapper {
  public class AWException extends exception {
  }
  

  Account thisAccount;
  

  public AccountWrapper(Account startWith) {
    thisAccount = startWith;
  }
  

  public Decimal getRoundedAvgPriceOfOpps() {
    AggregateResult[] aggResult = [
      SELECT AVG(Amount)
      FROM Opportunity
      WHERE accountId = :thisAccount.Id
      WITH USER_MODE
    ];
  

    Decimal average = (Decimal) aggResult[0].get('expr0');
    Long modulus = Math.mod(Integer.valueOf(average), 1000);
    Decimal returnValue = (modulus >= 500)
      ? (average + 1000) - modulus
      : average - modulus;
    if(returnValue <= 0) {
      throw new AWException('No won Opportunities');
    }
    return returnValue;
  }
  

  public Boolean isHighPriority() {
    if(getRoundedAvgPriceOfOpps() > 100000.00) {
      return true;
    }
    return false;
  }
}

Now let’s write the positive tests for the AccountWrapper class.

  1. In the Explorer sidebar, right-click the folder classes, then choose SFDX: Create Apex Class.
  2. Enter AccountWrapperTests as the name and accept the default directory.
  3. Replace the default contents with the following code.
    @IsTest
    private class AccountWrapperTests {
      @TestSetup
      static void loadTestData() {
        // GIVEN
        Account acct = TestFactory.getAccount('ACME', true);
        List<Opportunity> opps = TestFactory.generateOppsForAccount(acct.id, 1000.00, 5);
        insert opps;
      }
      @IsTest
      static void testPositiveRoundedAveragePrice() {
        // WHEN
        Account acct = [SELECT Id FROM Account LIMIT 1];
        AccountWrapper acctWrapper = new AccountWrapper(acct);
        // THEN
        Test.startTest();
          Assert.areEqual(
            acctWrapper.getRoundedAvgPriceOfOpps(),
            1000.00,
            'Expected to get 1000.00');
        Test.stopTest();
      }
    }
  4. Click File | Save.
  5. Right-click the file you’re working on, then choose SFDX: Deploy Source To Org.
  6. Click the Run Test button that appears on the testPositiveRoundedAveragePrice method.

Code Highlights

This test class uses an @TestSetup method that loads the test data. It also calls a factory method to generate five opportunities for each of your accounts.

With accounts and opportunities, you can test the logic of your AccountWrappergetRoundedAvgPriceOfOpps method. This method is designed to return the average price of all opportunities related to this account, rounded to the nearest thousand dollars. Because this is a positive test, the data you loaded and the opportunities you generated are logically consistent with what the method needs. In other words, your opportunities have positive amount values and an easily predictable rounded average. A good maxim for positive testing is: Good code produces expected results when you give it good inputs.

Write Negative Tests

Negative testing sounds so… negative. But many developers are positive that negative testing is crucial.

Check out this video for background on the what and why of negative testing.

Negative unit tests are useful for more than just making sure that every line of code is tested. Indeed, they’re sometimes more important than positive tests. Negative tests demonstrate that your code properly handles invalid data, unexpected user input, and changes in other parts of the code base. More importantly, negative tests affirm that the code you’ve written is fault-tolerant.

Testing how your code handles exceptions is as important as making sure your code works as intended during valid conditions. Imagine a batch process. Every night at 2 AM, your code processes account updates that your contacts have entered via the customer community. If the code fails on the first record in the batch, and it’s not gracefully handling exceptions, the code won’t process the remaining records. Negative testing helps ensure situations like that don’t happen.

So how can you write tests that pass when the code throws an exception? More importantly, because a number of exception types are available, how can you write tests that pass only when the code throws the expected type of exception?

Negative Test Pattern

The general pattern for negative tests looks like the following.

  1. Generate or load your test data.
  2. Start a try/catch block.
  3. Call Test.startTest().
  4. Execute your code.
  5. Call Test.stopTest().
  6. Make sure an exception is thrown as expected. You can do that by adding an assert that always fails on the next line. That statement should never be reached!
  7. Catch the expected exception, and if the exception message matches.

In our AccountWrapper class, we have a line of code that throws an exception. Here’s what a test exercising that would look like.

  1. Open VS Code.
  2. In the Explorer sidebar, click the folder classes.
  3. Select the class AccountWrapperTests class.
  4. Before the end of your class add the following code.
    @IsTest
    static void testNegativeAccountWrapperAvgPriceOfOpps() {
      // GIVEN
      Account acct = [SELECT Id FROM Account LIMIT 1];
      List<Opportunity> opps = [
        SELECT Amount
        FROM Opportunity
        WHERE accountId = :acct.Id
      ];
      for(Opportunity opp : opps) {
        opp.Amount = 0;
      }
      update opps;
      AccountWrapper acctWrapper = new AccountWrapper(acct);
      // WHEN
      try {
        Test.startTest();
          acctWrapper.getRoundedAvgPriceOfOpps();
        Test.stopTest();
        Assert.fail('An exception should have been thrown');
      } catch (AccountWrapper.AWException e) {
        // THEN
        Assert.isTrue(
          e.getMessage().equalsIgnoreCase('no won opportunities'),
          'Exception message does not match: ' + e.getMessage()
        );
      }
    }
  5. Click File | Save.
  6. Right-click the file you’re working on, then choose SFDX: Deploy Source To Org.
  7. Click the Run Test button that appears on the testNegativeAccountWrapperAvgPriceOfOpps method.

Code Highlights

In this test, you’re building on the opportunities that your @TestSetup method created. However, to test how AccountWrapper handles invalid data, you need to ensure that the opportunities created by the @TestSetup method all have an amount of 0. To do this, the test uses a for-loop to iterate over each opportunity and set the amount field to 0.

With the test data set up this way, the AccountWrapper’s getRoundedAvgPriceOfOpps method calculates a rounded price of 0 for all opportunities. This causes the code to throw an AWException, a custom exception type defined in the AccountWrapper class. It’s this AWException object that you catch in the unit test.

The thing about try/catch blocks like this, however, is that if they’re poorly written, the catch block can catch any instance, or subclass, of exception. The best practice is once you catch the exception, to have the test check the exception type and the exception message and details are what you expect. In this case, you’re expecting a custom exception type of AWException, which is defined in our AccountWrapper class.

Code often has multiple places where it can throw an exception—sometimes even the same type of exception. For instance, a ContactService class with a validate method might throw a ContactServiceException in three or four places and for different reasons. If we fail to check the type, message, and details in our test, we risk our tests incorrectly passing. This is called a false positive, and too many of these can erode trust in your tests.

Resources

Get Ready for the Hands-on Challenge

Note

You can retrieve code coverage results in VS Code for your Apex classes and Apex triggers every time you run one or more tests. To do this, edit your user or workspace settings, search for and check retrieve-test-code-coverage and then run your Apex tests. You can now see the code coverage in the Output panel, which shows the coverage percentage per Apex class and Apex trigger and lines that were not covered by the test run results.

Salesforce 도움말에서 Trailhead 피드백을 공유하세요.

Trailhead에 관한 여러분의 의견에 귀 기울이겠습니다. 이제 Salesforce 도움말 사이트에서 언제든지 새로운 피드백 양식을 작성할 수 있습니다.

자세히 알아보기 의견 공유하기