Every Salesforce team hits 75% Apex code coverage eventually. Most of them still ship bugs the tests should have caught. Coverage tells you what ran. It doesn’t tell you what got checked.
I’ve reviewed a lot of test classes over the years, across a fair few Salesforce orgs. The failure pattern repeats: a developer writes a test that exercises every line, asserts almost nothing, and calls it done. Deployment succeeds. Code review passes, because nobody reads test classes as closely as they read the actual logic. Then a bulk data load, or an edge case nobody thought about, finds the bug in production that the test never looked for.
Coverage measures execution, not correctness
The Salesforce test coverage number tells you which lines ran during a test. It says nothing about whether the test checked the right outcome, or any outcome at all. A test method can insert a record, call a method, and assert nothing, and Salesforce will count every line that executed anyway.
I’ve seen test classes with a dozen methods and maybe three actual System.assert statements between them. They pass. They deploy. As regression protection they’re close to useless, and that’s the real cost. Six months later someone changes the trigger logic, nothing fails in CI, right up until a support ticket comes in.
The three tells of a coverage-only test
- It inserts one record, runs the method once, and checks that no exception was thrown. Nothing about the actual result.
- It wraps everything in
Test.startTest()andTest.stopTest()but never queries the database afterward to confirm the state changed the way it should. - It only tests the happy path. No bad input, no missing field, no record that’s already sitting in a status the code doesn’t expect.
None of these are hard to spot in review. The problem is most reviewers are scanning for the coverage percentage on the deployment pipeline, not reading the test body line by line.
What actually catches bugs before production does
Test bulk, every time
A trigger that works fine for one record and falls over at 150 is one of the most common incidents I’ve seen land in a support queue. Governor limits don’t show up in a single-record test. Write every trigger test against a batch, 200 records at minimum, because that’s the ceiling Salesforce enforces on a single DML operation, and it’s exactly where SOQL-in-a-loop and other bulkification mistakes surface.
A contact-centre team I worked with had a case-assignment trigger that passed every test for two years. It queried a related object once per case, inside a loop. Fine for the sandbox tests, which always inserted one or two cases. Then a data migration loaded 3,000 cases in one batch and the transaction hit the SOQL query limit halfway through. The fix took an afternoon. Finding it took three days, because nobody had a bulk test that would have caught it in five seconds.
Test the failure paths on purpose
Negative testing gets skipped constantly, and it’s where the real bugs live. What happens when a required field is null? When a callout times out? When a record is locked by another process? When the user running the code doesn’t have edit access to the field your trigger is trying to update?
Write a test for each of those, expect the exception, and assert on the actual error message or the DmlException type. If your Apex has a try/catch block, you need a test that proves the catch block does what you think it does. A surprising number of catch blocks quietly swallow errors that should have surfaced somewhere.
Run tests as the people who’ll actually use the feature
System.runAs() lets you execute a test in the context of a specific user, with their profile, their permission sets, their field-level security. Skip this and you’re testing as the org admin, who can see and edit everything. Deploy the same logic to a support agent on a restricted profile and it can fail in ways the test suite never flagged, usually a silent no-op where a field update just gets dropped rather than a clean exception.
This matters more once sharing rules get involved. A “with sharing” class behaves differently depending on who’s running it. If your tests never run as a restricted user, you’re not testing the sharing model at all, just the Apex syntax.
Build test data that looks like production, not a demo
Real records have blank optional fields, statuses that skip stages, near-duplicate names, and relationships that don’t always resolve cleanly. A @TestSetup method that inserts three perfectly formed accounts with every field populated will never surface the null-pointer exception that shows up when a real account is missing its billing address.
Build a small test data factory for your org’s core objects and reuse it everywhere. It’s less code overall than everyone hand-rolling their own test records, and when someone deliberately builds a messy edge case into the factory, every test in the org benefits from it at once.
A short checklist for reviewing test classes
When I review someone else’s Apex tests, I’m looking for a handful of things before I even check the coverage number:
- Does every test method have at least one meaningful assertion, checking a value, not just confirming nothing threw?
- Is there a bulk test with 150 to 200 records for anything that touches a trigger or a batch class?
- Is there at least one test that deliberately fails, and asserts on how it fails?
- Does anything sharing-sensitive get tested with
System.runAs()as a restricted user? - Are HTTP callouts mocked with
HttpCalloutMock, rather than skipped with scatteredTest.isRunningTest()checks in production code?
That last point deserves its own mention. I still see Test.isRunningTest() used inside actual business logic to skip a callout during tests. It works, technically. But it means the code path that runs in production is never the code path your test actually executes. A proper mock class takes more setup the first time, and it means the test is testing something real.
Let coverage follow, don’t chase it
Aim for tests that would catch a real bug if someone introduced one tomorrow: the bulk test, the negative test, the runAs test, the mocked callout. Do that consistently and the coverage percentage takes care of itself, usually well past 75% without anyone tracking the number directly.
The org I trust is the one where a failing test means something actually broke. Not the one where every test class is green and the production incidents keep rolling in anyway.