---
title: "Coverage without useless tests"
url: "https://trailassembly.com/apex-code-coverage/"
topic: "Apex and Development"
published: "2026-09-12T02:46:58+00:00"
updated: "2026-10-06T13:53:13+00:00"
---

# Coverage without useless tests

**The short version:** 75% org-wide is a deployment gate, not a quality target, and it is trivially satisfied by tests that assert nothing. The useful question is not what percentage you have but whether a test would fail if the code were wrong. Those are different properties and only one of them is measured.

## What the number actually requires

- **75% across the org** to deploy to production. Not per class.

- **Every trigger needs some coverage** — more than zero lines, which is a separate rule.

- **[Managed package](https://trailassembly.com/1gp-vs-2gp/) code does not count** toward your total, in either direction.

- **Test classes themselves are excluded** from the calculation.

So a single class with 5,000 well-covered lines can carry a lot of untested code past the gate. That is not a loophole to use, it is an explanation of why an org at 82% can still have critical logic with no test at all.

## The test that covers everything and proves nothing

```apex
@IsTest
static void coverIt() {
    Account a = new Account(Name = 'x');
    insert a;
    AccountService.recalculate(a.Id);
}
```

This runs the code, so the lines are covered. It asserts nothing, so it passes whether `recalculate` computes correctly, computes wrongly, or does nothing at all. Remove the body of the method and the test still passes.

A test with no [assertion](https://trailassembly.com/apex-assert-exception-message/) is worse than no test, because it produces a green build and a number that satisfies the gate. If you find these, the honest first step is to add assertions, not to delete them — deleting drops your coverage and the gate is real.

## What to assert

Three things, in order of value:

**The output.** The value returned, or the field set, compared against a number you worked out by hand. Not against the same calculation re-implemented in the test, which passes even when both are wrong.

**The side effects.** If the method is supposed to create a task, query for it. If it is supposed to leave something unchanged, assert that too — the tests that catch real regressions are often the ones asserting nothing happened.

**The failure.** Call it with input that should be rejected and assert the exception type and something about the message. Around half of production incidents are a failure path that was never executed once.

```apex
try {
    OrderService.submit(orderWithNoLines);
    System.assert(false, 'expected an exception for an order with no lines');
} catch (OrderValidationException e) {
    System.assert(e.getMessage().contains('at least one line'), e.getMessage());
}
```

The `System.assert(false, ...)` after the call is what makes this a real test. Without it, a method that stops throwing passes silently.

## Bulk is a correctness test, not a coverage one

Insert 200 records, not one. It costs one line and catches the entire class of bug that only appears under Data Loader, because a [query inside a loop](https://trailassembly.com/soql-101/) covers exactly the same lines with one record as with two hundred. The limits it protects against are per transaction, and they are in the [governor limits reference](https://trailassembly.com/governor-limits-reference/).

## Reading the coverage report usefully

The Developer Console and `sf apex run test --code-coverage` both show uncovered lines. Look at which lines rather than at the percentage. Uncovered `catch` blocks, uncovered `else` branches and uncovered null checks are the ones worth fixing, because they are the paths that run when something goes wrong, which is when you most want the code to be right.

A class at 100% with every assertion removed is a worse state than a class at 76% where the tests check the arithmetic.

## What to stop doing

- **`SeeAllData=true`** unless the API genuinely requires it. It makes tests depend on org data that changes, and a sandbox refresh breaks them.

- **Testing getters and setters** to lift a percentage. It adds maintenance and finds nothing.

- **One enormous test method** covering a whole class. When it fails you learn that something broke, not what.

## Frequently asked

**Does a deployment run all tests?** Depends on the [test level](https://trailassembly.com/salesforce-test-levels/). `RunLocalTests` excludes managed packages and is the usual choice. Production deployments of Apex require tests regardless.

**Why did coverage drop after a deployment I did not change?** Coverage is calculated from the last run, so new uncovered code anywhere moves the org-wide number. It is a shared total.

**Is 100% a goal?** No. Some lines are not worth the test that would cover them. Aim for every branch that decides something.

---

Source: https://trailassembly.com/apex-code-coverage/
Trail Assembly — independent Salesforce write-ups.
