Skip to main content

Write Negative Tests

Learning Objectives

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

  • Explain what negative unit tests are.
  • Write negative unit tests.

Why Write Negative Unit 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 important, 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.

The Pattern

Positive testing tests your code with valid inputs to demonstrate what happens when your code functions properly. Negative testing demonstrates that your code properly handles invalid data and exceptions. That is not always easy to accomplish. After all, throwing an exception during a unit test causes that test to fail. 

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? 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 match.

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 o : opps) {
        o.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 written badly, 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. 

Often code 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.

Share your Trailhead feedback over on Salesforce Help.

We'd love to hear about your experience with Trailhead - you can now access the new feedback form anytime from the Salesforce Help site.

Learn More Continue to Share Feedback