SapotaCorp

The 75 Percent Apex Coverage Myth: Lines Covered, Logic Untested

Salesforce requires 75 percent code coverage to deploy to production. Most teams treat the number as a goal. The number is the floor. Hitting it with tests that have no meaningful assertions produces an org full of code nobody trusts.

The 75 Percent Apex Coverage Myth: Lines Covered, Logic Untested

Key takeaways

  • 75 percent Apex coverage is the deployment floor, not a quality goal. Hitting 75 with tests that exercise lines without asserting outcomes produces code nobody trusts. Sapota's standard on engagements is 85 percent or higher with meaningful assertions on every test method.
  • Line coverage measures whether code executed; it does not measure whether the code behaved correctly. A test that calls a method but does not assert anything counts toward coverage. Production teams audit test methods for assertion density, not just coverage percentage.
  • Mutation testing is the conceptual frame for meaningful coverage. Imagine swapping one operator (== to !=, + to -); if no test fails, the test suite did not actually verify that line. Salesforce orgs do not run mutation tools by default; the discipline is mental — write assertions that would catch swapped operators.
  • Edge case coverage matters more than happy-path coverage. The happy path runs every day in production; bugs surface on edge cases (null values, boundary conditions, error paths). Test design that explicitly covers edge cases produces a test suite that catches real regressions.

Salesforce requires 75 percent code coverage to deploy Apex to production. The rule is well known. Most teams treat 75 percent as a goal: hit the number, deploy, move on. The result is orgs where every Apex class has tests, every test has coverage, and nobody trusts the test suite to catch real bugs.

The 75 percent number is the floor, not the ceiling. Coverage is necessary but not sufficient. A test that runs every line of a method but asserts nothing about the output meets the platform requirement and provides zero confidence. The work is in writing assertions that catch regressions, not in chasing the coverage number.

What 75 percent coverage actually measures

The platform counts code coverage as: number of executable Apex lines exercised by at least one test, divided by total executable Apex lines. The threshold:

  • Sandbox deployments: any coverage works.
  • Production deployments: 75 percent overall, with no class at 0 percent.

What this measures: did a line of code run during testing.

What this does not measure:

  • Whether the code produced the right output.
  • Whether the test would notice if the code's behavior changed.
  • Whether edge cases were exercised.
  • Whether failure paths were tested.

A test that calls a method and asserts nothing about the result hits the same coverage as a test with extensive assertions. The platform cannot tell the difference.

The empty-assertion anti-pattern

The most common form of meaningless coverage:

@isTest
static void testCalculateDiscount() {
  Account a = TestDataFactory.createAccount();
  Decimal result = DiscountService.calculate(a);
  // No assertion.
}

The method ran. The lines covered. The coverage report shows 100 percent for DiscountService.calculate(). If a developer changes the method to return -1 always, the test still passes.

Variants of the same anti-pattern:

  • System.assert(true); at the end.
  • System.assertNotEquals(null, result); (too permissive).
  • System.assert(result.size() >= 0); (always true).

These all run the code without checking what it does. They satisfy coverage. They catch no bugs.

The mutation-testing thought experiment

A useful test of whether a test suite is meaningful: imagine someone makes a deliberate bug. Does any test catch it?

For each method, list the kinds of mutations that could happen:

  • Return a wrong value.
  • Skip a step in a multi-step calculation.
  • Apply the wrong condition in a branch.
  • Use the wrong field.
  • Reverse a comparison (> to <).
  • Return null when a real value was expected.

A test suite catches mutations when at least one test would fail for each mutation. A 100 percent coverage suite that catches zero mutations is useless. A 75 percent coverage suite that catches 90 percent of mutations is solid.

Apex does not have automated mutation testing tools the way Java or Python does. The mental exercise is the substitute: when writing assertions, ask "what mutation would this assertion catch?" If no useful mutation, the assertion is decorative.

What meaningful assertions look like

A test with real assertions:

@isTest
static void testCalculateDiscount_premiumCustomer() {
  Account a = TestDataFactory.createAccount(
    new Map<String, Object>{'Tier__c' => 'Premium'}
  );

  Decimal result = DiscountService.calculate(a);

  // Catches: wrong tier mapping, wrong percentage, return-zero bugs.
  System.assertEquals(0.15, result,
    'Premium tier should get 15% discount');
}

@isTest
static void testCalculateDiscount_standardCustomer() {
  Account a = TestDataFactory.createAccount(
    new Map<String, Object>{'Tier__c' => 'Standard'}
  );

  Decimal result = DiscountService.calculate(a);

  // Catches: cross-contamination between tiers, default-value bugs.
  System.assertEquals(0.05, result,
    'Standard tier should get 5% discount');
}

@isTest
static void testCalculateDiscount_noTier() {
  Account a = TestDataFactory.createAccount(
    new Map<String, Object>{'Tier__c' => null}
  );

  Decimal result = DiscountService.calculate(a);

  // Catches: null handling, default fallback.
  System.assertEquals(0, result,
    'No tier should return zero discount');
}

Three tests, each exercising a different input class, each asserting the expected output exactly. Mutations to the method (wrong percentage, wrong tier mapping, missing null check) will fail at least one test.

The third test (the null case) is the most valuable. The happy paths often work; the edge cases are where bugs hide.

Edge cases to test by category

For data manipulation:

  • Empty input (null, empty list, empty string).
  • Single record vs multiple records.
  • Maximum platform limits (200 trigger batch, 10,000 records for some operations).
  • Negative numbers, zero, very large numbers.
  • Unicode and special characters in strings.

For business logic:

  • Each branch of every if and switch.
  • Each value of significant enums or picklists.
  • Both sides of every threshold or comparison.

For integration code:

  • Success response.
  • Each documented error response (4xx, 5xx).
  • Timeout.
  • Malformed response (invalid JSON, missing fields).

For triggers:

  • Insert, update, delete, undelete cases.
  • Bulk insert (200 records).
  • Records with related objects.
  • Records that trigger conditional logic vs records that do not.

A trigger handler that covers all these cases has 8-12 tests, not 2-3. The line coverage might land at 90 percent or 95 percent. The mutation coverage is dramatically higher than a coverage-only suite.

When to use Test.startTest and Test.stopTest

The platform resets governor limits inside Test.startTest/Test.stopTest boundaries. Three reasons to use them deliberately:

1. Async work completion. Async work enqueued before Test.stopTest runs when stopTest is called. Without the wrapper, async work never executes during the test.

2. Limit isolation. Test setup code (creating data) consumes governor limits. Wrapping the actual test action inside startTest/stopTest gives the action a fresh limit budget. Useful when the test setup itself is heavy.

3. Standard convention. Code reviewers expect the pattern. Tests without it look incomplete.

@isTest
static void testTriggerWithAsyncCallout() {
  Account a = TestDataFactory.createAccount();

  Test.setMock(HttpCalloutMock.class, new SimpleMock());

  Test.startTest();
  a.Sync_To_External__c = true;
  update a; // Trigger fires, enqueues Queueable.
  Test.stopTest(); // Queueable executes here.

  // Assert post-callout state.
  Account refreshed = [SELECT External_Status__c FROM Account
                       WHERE Id = :a.Id];
  System.assertEquals('Synced', refreshed.External_Status__c);
}

Sapota's test quality checklist

The patterns that distinguish meaningful test suites from coverage-chasing ones:

  • Every test asserts at least one specific value. No empty-assertion tests.
  • Each method has 2-4 tests covering different input classes. Happy path plus at least one edge case.
  • Bulk tests use 200 records. Bulkification gets exercised, not assumed.
  • Async tests use Test.startTest/Test.stopTest. Async work actually runs.
  • Failure paths tested explicitly. Mock failures, exception handlers, retry logic all covered.
  • Coverage tracked but not chased. 85-90 percent overall is the realistic target; 75 percent is the floor.

When tests reveal architectural problems

Sometimes the difficulty of writing a meaningful test reveals a design problem. A method that does five things needs 50 tests to cover all permutations; splitting it into five methods makes each testable in isolation.

Patterns that signal a refactor:

  • Tests that require massive setup. Maybe the code has too many dependencies.
  • Tests that look identical except for one input value, repeated 20 times. Maybe the code branches on input that should be polymorphic.
  • Tests that cannot avoid using mocks for internal collaborators. Maybe the code is doing two unrelated things in one place.

The test suite is a design feedback tool. Bad tests usually point at bad code.

Common test suite anti-patterns

Five patterns Sapota sees in audits:

  • Coverage rush. Tests added at the last minute to pass deployment. No assertions, just method invocations to bump the number.
  • The big setup test. One test that creates 50 records and exercises every method. Fails for unknown reasons, debugging takes hours.
  • Happy path only. Every test creates a valid record and verifies success. Failure paths never run. Real bugs always come from failure paths.
  • Tests that re-implement the code. The test's assertion is System.assertEquals(method(input), expected) where expected = method(input). Tautology.
  • No bulk variant for triggers. Trigger tested with 1 record. Production hits 200, governor limits, incident.

What good test discipline looks like

A Salesforce org with healthy test discipline:

  • 85+ percent overall coverage with meaningful assertions on every test.
  • Bulk variants for every trigger and async handler.
  • Failure path tests for every external integration.
  • A test data factory that handles required-field changes once.
  • Code review explicitly checks for assertion quality, not just line coverage.

Sapota's Salesforce team holds the Platform Developer I credential and treats test quality as a code review gating criterion on every engagement. The 75 percent threshold is a deployment requirement, not a quality target.


Auditing test coverage quality or improving Apex test discipline? Sapota's Salesforce team handles test refactoring, mutation analysis, and review-process design on production engagements. Get in touch ->

See our full platform services for the stack we cover.

Contact Us Now

Share Your Story

We build trust by delivering what we promise – the first time and every time!

We'd love to hear your vision. Our IT experts will reach out to you during business hours to discuss making it happen.

WHY CHOOSE US

"Collaborate, Elevate, Celebrate where Associates - Create Project Excellence"

SapotaCorp beyond the IT industry standard, we are

  • Certificated
  • Assured quality
  • Extra maintenance

Tell us about your project