Salesforce Dictionary - Free Salesforce GlossarySalesforce Dictionary
All errors
Deployment

Average test coverage across all Apex Classes and Triggers is XX%, at least 75% required

A production deploy of Apex requires at least 75% line coverage *org-wide*, and 100% on triggers (including triggers that aren't part of your deploy). The error doesn't always tell you which class dragged the average down — you have to compute it.

Also seen asAverage test coverage·75% required·code coverage below 75·Code coverage requirement

The release team queues up a production deploy on a Tuesday afternoon. The package is fifty-eight Apex classes, twelve triggers, and a Lightning Web Component bundle. Validation runs for thirty minutes and fails with Average test coverage across all Apex Classes and Triggers is 73%, at least 75% required for production deployment. Nobody is sure which class dropped coverage. The deploy is blocked. Friday's release date is at risk.

What the platform is checking

Salesforce requires that aggregate code coverage across all Apex classes and triggers in a production deploy reach 75%. The platform calculates coverage by running every test class in the deploy (and, in some deploy modes, every existing test in the target org), measuring which executable lines of Apex were touched during the test run, and dividing by the total executable lines across all classes and triggers in scope.

The 75% threshold applies to the aggregate, not to any single class. A class with 40% coverage is allowed as long as other classes have enough coverage to lift the overall average above 75%. The threshold also doesn't apply to non-Apex metadata (Visualforce pages, Lightning components, flows). Only Apex lines count toward the coverage calculation.

Two coverage rules matter beyond the headline 75% number. Every trigger must have at least 1% coverage; a trigger with zero test coverage blocks the deploy even if the aggregate is fine. And specific tests must succeed, not just run. A test that throws but is caught by a pre-existing try/catch in the test method still counts as passing, but a test method that fails an assertion or throws an uncaught exception blocks the deploy regardless of coverage numbers.

The validator runs the tests in the target org, not in the source sandbox. A test that passes in your sandbox can fail in production if the production data, profile, or permission set context differs. Coverage that looks fine in the source can drop in the target if specific code paths only execute under sandbox-specific data.

The broken example

A common shape: a service class with a complex method that the tests barely touch.

public class OpportunityValueCalculator {
    public static Decimal computeAdjusted(Opportunity opp) {
        Decimal base = opp.Amount;
        if (opp.StageName == 'Closed Won') {
            base = base * 1.0;
        } else if (opp.StageName == 'Negotiation/Review') {
            base = base * 0.8;
        } else if (opp.StageName == 'Proposal/Price Quote') {
            base = base * 0.6;
        } else if (opp.StageName == 'Needs Analysis') {
            base = base * 0.3;
        } else {
            base = base * 0.1;
        }

        if (opp.AccountId != null) {
            Account a = [SELECT Industry, Type FROM Account WHERE Id = :opp.AccountId];
            if (a.Industry == 'Technology') {
                base = base * 1.1;
            }
            if (a.Type == 'Customer - Direct') {
                base = base * 1.05;
            }
        }

        return base;
    }
}

The test only covers the Closed Won path:

@isTest
private class OpportunityValueCalculatorTest {
    @isTest
    static void testClosedWon() {
        Opportunity opp = new Opportunity(Name='Test', StageName='Closed Won', Amount=100, CloseDate=Date.today());
        insert opp;
        Decimal result = OpportunityValueCalculator.computeAdjusted(opp);
        System.assertEquals(100, result);
    }
}

The other four stage branches and the account-lookup logic are untouched. The class's coverage drops to about 30%. If this class accounts for a meaningful fraction of the org's Apex lines, the deploy fails on the aggregate threshold.

The fix, three paths

Add tests for the uncovered branches. The first-best answer is to write tests for each uncovered code path. Coverage is a side effect of meaningful testing, not the goal. Test each stage value, test the account-lookup logic with and without an account, test edge cases like null Amount.

Reduce the surface area being measured. If a method is dead code, delete it. Removing the uncovered method removes the lines from the denominator, which raises the aggregate. Dead code in production is a liability; identifying and removing it during a coverage crisis pays dividends beyond the immediate deploy.

Refactor complex methods into smaller, more testable pieces. A 40-line method with five branches is harder to test than five 8-line methods with one branch each. Refactoring lets each piece be tested in isolation. Coverage rises, and the code is easier to maintain.

The fixed example

The same class with proper coverage:

@isTest
private class OpportunityValueCalculatorTest {
    @isTest
    static void testClosedWon() {
        Opportunity opp = new Opportunity(Name='T1', StageName='Closed Won', Amount=100, CloseDate=Date.today());
        insert opp;
        System.assertEquals(100, OpportunityValueCalculator.computeAdjusted(opp));
    }

    @isTest
    static void testNegotiation() {
        Opportunity opp = new Opportunity(Name='T2', StageName='Negotiation/Review', Amount=100, CloseDate=Date.today());
        insert opp;
        System.assertEquals(80, OpportunityValueCalculator.computeAdjusted(opp));
    }

    @isTest
    static void testProposal() {
        Opportunity opp = new Opportunity(Name='T3', StageName='Proposal/Price Quote', Amount=100, CloseDate=Date.today());
        insert opp;
        System.assertEquals(60, OpportunityValueCalculator.computeAdjusted(opp));
    }

    @isTest
    static void testNeedsAnalysis() {
        Opportunity opp = new Opportunity(Name='T4', StageName='Needs Analysis', Amount=100, CloseDate=Date.today());
        insert opp;
        System.assertEquals(30, OpportunityValueCalculator.computeAdjusted(opp));
    }

    @isTest
    static void testAccountAdjustments() {
        Account a = new Account(Name='Tech Co', Industry='Technology', Type='Customer - Direct');
        insert a;
        Opportunity opp = new Opportunity(Name='T5', StageName='Closed Won', Amount=100, CloseDate=Date.today(), AccountId=a.Id);
        insert opp;
        Decimal result = OpportunityValueCalculator.computeAdjusted(opp);
        System.assertEquals(115.5, result, 'Account adjustments should compound');
    }
}

Five tests, each exercising a specific path. Coverage on the class climbs to about 95% and the aggregate impact resolves.

Finding the low-coverage classes

When the deploy fails, the error message doesn't always name the worst offender. To find it:

Open Setup, Apex Test Execution, click View Test History. Each test run shows per-class coverage. Sort by Code Coverage ascending and the low-coverage classes float to the top.

Or run the Tooling API query directly:

SELECT ApexClassOrTrigger.Name, NumLinesCovered, NumLinesUncovered
FROM ApexCodeCoverageAggregate
ORDER BY NumLinesUncovered DESC
LIMIT 20

The top of the list is where to focus. A class with 500 uncovered lines and 100 covered ones drags the aggregate down more than ten classes with 5 uncovered lines each.

The 1% trigger rule

Every trigger must have at least 1% test coverage. A trigger with zero coverage blocks the deploy even if the aggregate is fine.

The 1% rule sounds trivial but catches a common case: a developer adds a new trigger and forgets to write any test for it. The trigger body might be one line that calls a well-tested handler, but the trigger file itself shows zero coverage. The deploy fails not because the logic is untested but because the trigger declaration line is uncovered.

The fix is a minimal test that causes the trigger to fire:

@isTest
static void testCaseTriggerFires() {
    Case c = new Case(Subject='Test', Status='New', Origin='Web');
    insert c;
    // The trigger ran; coverage is non-zero for the trigger file
}

One line of insert in a test method covers the trigger declaration and any conditional dispatch the trigger does. That's enough to clear the 1% rule.

Production vs sandbox test mode

A deploy to production runs in RunAllTestsInOrg mode by default, which executes every test in the destination. A deploy to a sandbox can run in RunSpecifiedTests, RunLocalTests, or NoTestRun modes, depending on what the developer selected.

This means a deploy that succeeded in a sandbox under RunSpecifiedTests can fail in production under RunAllTestsInOrg because tests in the production org (which aren't in the deploy package) regress. A test in the production org that was passing might start failing because of code changes in the deploy package.

Always validate against production with a check-only deploy and RunAllTestsInOrg before scheduling the real deploy. The validation reveals which existing tests break and gives time to fix them.

Edge case: tests that depend on org configuration

A test that creates an Opportunity and expects the StageName picklist to contain Closed Won will fail if the production org has been customized to remove that stage. The test method itself is fine, but it's environmentally fragile.

The fix is to make tests resilient to org configuration. Read picklist values dynamically:

List<Schema.PicklistEntry> stages = Opportunity.StageName.getDescribe().getPicklistValues();
String firstStage = stages[0].getValue();

Or seed test data through utilities that adapt to the target org's schema. Tests should encode logic, not specific picklist string literals that might shift between orgs.

Edge case: tests that touch real data

A test method without the @isTest(SeeAllData=true) annotation is isolated from real org data; SOQL queries inside the test only see records created within the test. This is the right default.

A test that explicitly opts in to SeeAllData=true can pass in a sandbox that has the right reference records and fail in production if those records don't exist. The fix is usually to remove SeeAllData=true and create the necessary fixtures inside the test method. Modern Apex testing avoids the annotation.

A subtle case: tests that throw during setup

If a test method throws during @TestSetup or before the first assertion, the test is reported as failing, but no coverage is recorded for any code it would have exercised. A test that crashes early can drag down the aggregate twice: it fails (which can block the deploy on its own) and contributes zero to coverage.

The fix is to ensure @TestSetup is reliable. Wrap risky setup in try/catch only if you really need to swallow failures; usually it's better to let the setup fail loudly so you can fix the root cause. Test setup that depends on org state should be moved into per-test code where it can be tested in isolation.

A subtle case: code that runs only in production

A class that branches on System.UserInfo.getUserType() or on a configuration flag that's only set in production will have branches the tests can't easily reach. Coverage on the production-only branches stays low forever.

The fix is to refactor those branches behind an interface. Production injects the production implementation; tests inject a test double. The branch logic moves out of the class and into the dependency injection layer. The class itself becomes testable end-to-end.

public interface EnvironmentInfo {
    Boolean isProduction();
}

public class ProdEnvironmentInfo implements EnvironmentInfo {
    public Boolean isProduction() { return UserInfo.getUserType() == 'Standard'; }
}

@isTest
public class TestEnvironmentInfo implements EnvironmentInfo {
    private Boolean prodValue;
    public TestEnvironmentInfo(Boolean v) { this.prodValue = v; }
    public Boolean isProduction() { return prodValue; }
}

The class under test takes an EnvironmentInfo in its constructor. Production wires the prod implementation; tests wire the test double with the value they want. Coverage rises because the branches are now testable.

Defensive habits

Run all tests against a check-only deploy weekly. Even if no deploy is scheduled, the check confirms that the org's overall test suite still passes the 75% threshold. Drift happens silently; a new class added without tests can drop the aggregate over time.

Track coverage by class over time. A weekly job that pulls ApexCodeCoverageAggregate and saves it to a custom object or external system reveals trends. A class whose coverage is steadily declining is a class that's being modified without test updates. The trend matters more than the absolute number.

Require tests in pull-request review. Code review that explicitly checks "is this new logic covered by tests?" prevents coverage erosion at the source. The marginal cost of asking is low and the cumulative value is high. Make it a PR template item so it's never forgotten.

Avoid coverage-only tests. A test that calls every method without asserting anything meaningful does raise coverage numbers but provides no real protection. Reviewers should look for assertions, not just method calls. A test without an assertion is documentation that the method runs without crashing, which is sometimes enough but usually not.

Quick recovery checklist

For a production deploy blocked by coverage:

  1. Run the Tooling API query above to identify the lowest-coverage classes.
  2. For each class in the top five, write meaningful tests for the uncovered branches.
  3. Re-validate the deploy with check-only.
  4. If the aggregate is now above 75%, schedule the real deploy.
  5. After the deploy, run a coverage retrospective. Why was coverage low? Was a class added without tests? Was a test method deleted? Capture the pattern and update team practices.

Most coverage-blocked deploys resolve in a day or two. The longer ones uncover deeper structural issues with how the codebase is tested, and those are worth investing in once you've earned the team's attention with the immediate fix.

Further reading from Salesforce

Related dictionary terms

Share this fix

Share on LinkedInShare on X

Related Deployment errors