TrueSolv — Header Component
Home >> How to's >> Apex Unit Test Best Practices Beyond the Coverage Number

Apex Unit Test Best Practices Beyond the Coverage Number

Apex unit test code example showing assertions, bulk data, and a CI pipeline running tests before deploy

Plenty of orgs hit the 75 percent coverage requirement with tests that check nothing real at all.

Coverage counts which lines of code ran during a test, not whether the test verified anything meaningful. A test that inserts a record, calls a method, and confirms only that no exception was thrown will happily push coverage past 75 percent while catching zero actual regressions. Real Apex unit test best practices start from a different question: whether the test would fail the moment the underlying logic breaks, not whether it satisfies a percentage.

APEX TESTING / CI PIPELINE 75% Coverage number alone Real assertions Bulk data, 200+ Mocked callouts CI gate Deploy What actually gates the merge Assertions tied to business outcome, not just the absence of an error 200+ record bulk test on every method that touches DML or SOQL External callouts mocked, never live inside a test context
From a 75% coverage number to a CI gate that actually stops a broken assumption from shipping.

The gap between passing and protecting

A weak test looks productive. It compiles, it runs, it shows green in the deployment log, and the coverage number goes up. What it usually skips is any assertion tied to business outcome. It checks that a record exists, not that a field landed on the value the logic was supposed to produce. Six months later, someone changes a trigger, every existing test still passes, and the bug ships anyway — because nothing in the suite was actually watching for it.

Assert patterns that actually catch something

A test worth keeping asserts on the specific outcome the code is supposed to produce, not just on the absence of an error. That means asserting the field value that should have changed, the record count that should have resulted, or the exception that should have been thrown on a bad input. Here is a pair of tests for a trigger that updates a custom forecast category once an opportunity crosses an amount threshold.

@isTest
private class OpportunityRoundingTest {

  @isTest
  static void updatesForecastWhenAmountChanges() {
    Account acc = new Account(Name = 'Test Account');
    insert acc;

    Opportunity opp = new Opportunity(
      Name = 'Renewal Deal',
      AccountId = acc.Id,
      StageName = 'Prospecting',
      CloseDate = Date.today().addDays(30),
      Amount = 10000
    );
    insert opp;

    Test.startTest();
    opp.Amount = 25000;
    update opp;
    Test.stopTest();

    Opportunity updated = [
      SELECT Forecast_Category_Custom__c, Amount
      FROM Opportunity WHERE Id = :opp.Id
    ];

    Assert.areEqual(25000, updated.Amount,
      'Amount should reflect the update');
    Assert.areEqual('Best Case', updated.Forecast_Category_Custom__c,
      'Forecast category should move once the deal crosses the threshold');
  }

  @isTest
  static void doesNotDefaultAmountWhenLeftBlank() {
    Account acc = new Account(Name = 'Test Account 2');
    insert acc;

    Opportunity opp = new Opportunity(
      Name = 'Early Deal',
      AccountId = acc.Id,
      StageName = 'Prospecting',
      CloseDate = Date.today().addDays(60)
    );

    Test.startTest();
    insert opp;
    Test.stopTest();

    Opportunity result = [SELECT Amount FROM Opportunity WHERE Id = :opp.Id];
    Assert.isNull(result.Amount,
      'A deal with no amount should stay null, not default to zero');
  }
}
Both tests assert on the actual business rule — the forecast threshold and the null default — not just on the record existing.

Bulk data catches what a single record never will

A test that inserts one record will pass even when the underlying code runs a query or a DML statement inside a loop, because one iteration never comes close to a governor limit. Two hundred records will. Building bulk data into a test is the difference between finding that bug in a sandbox and finding it in production during a mass import.

@isTest
static void handlesBulkInsertWithoutHittingLimits() {
  List<Account> accounts = new List<Account>();
  for (Integer i = 0; i < 200; i++) {
    accounts.add(new Account(Name = 'Bulk Account ' + i));
  }

  Test.startTest();
  insert accounts;
  Test.stopTest();

  List<Account> inserted = [
    SELECT Id FROM Account WHERE Name LIKE 'Bulk Account%'
  ];
  Assert.areEqual(200, inserted.size(),
    'All 200 accounts should insert without hitting a governor limit');
}

Mocking callouts without touching a real endpoint

Any test that reaches out to a real external system is slow, flaky, and eventually blocked by Salesforce itself, since live callouts aren't allowed inside a test context. The fix is the HttpCalloutMock interface, which lets a test hand back a fake response and check that the code parses and handles it correctly — success and failure alike.

@isTest
private class ShippingRateMockTest {

  private class SuccessMock implements HttpCalloutMock {
    public HttpResponse respond(HttpRequest req) {
      HttpResponse res = new HttpResponse();
      res.setStatusCode(200);
      res.setBody('{"rate": 14.50, "carrier": "Standard"}');
      return res;
    }
  }

  @isTest
  static void parsesRateFromSuccessfulCallout() {
    Test.setMock(HttpCalloutMock.class, new SuccessMock());

    Test.startTest();
    ShippingRate rate = ShippingService.getRate('90210');
    Test.stopTest();

    Assert.areEqual(14.50, rate.amount,
      'Rate should match the mocked response body');
    Assert.areEqual('Standard', rate.carrier,
      'Carrier should be parsed from the JSON response');
  }
}
A second mock returning a 500 status covers the failure path that actually breaks integrations in production.

Wiring tests into CI before anything merges

None of this holds up if tests only run when someone remembers to run them by hand. Wiring Apex tests into a CI pipeline means every pull request triggers a deploy to a scratch org or sandbox followed by a full test run, and a merge simply can't happen if a test fails or coverage drops below the threshold.

name: apex-tests
on: [pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: sf org login sfdx-url --sfdx-url-file authFile.txt --alias ci-org
      - run: sf project deploy start --target-org ci-org
      - run: sf apex run test --target-org ci-org \
               --code-coverage --result-format human --wait 20

A coverage number without CI is a report nobody reads until deploy day. A coverage number wired into every pull request is a gate that actually stops a broken assumption from shipping.

Send your test suite our way through the contact form for a custom development and code review pass, and follow TrueSolv on LinkedIn for more Apex notes.

Share:
Free Consultation

Ready to solve your Salesforce challenges?

Get a free consultation with our certified Salesforce experts. No commitment required.

See our packages →
TrueSolv — Footer Component