Back to insights

Apex Testing, Quality & Release Confidence

An Apex test class is not a coverage generator

JSBC Labs8 min read

Coverage tells you where execution went—not whether it was right

A deployment fails because a class sits at 72% coverage. The team adds a test that inserts a record, calls the method and makes no meaningful assertion. Coverage rises above the threshold, the pipeline turns green and the release proceeds. Production then creates the wrong entitlement because the test exercised the branch without proving its result. The metric improved; confidence did not.

The JSBC Labs position is that an Apex test is an executable statement of behaviour. Salesforce requires at least 75% Apex coverage for deployment or packaging, but its own documentation says coverage does not guarantee test effectiveness. Treat coverage as a map of unvisited code. The release signal comes from assertions that distinguish the intended outcome from a plausible but incorrect one.

Name the behaviour before arranging the data

A test called testMethod1 encourages readers to infer purpose from setup noise. Name the scenario, action and outcome: closing a qualified Opportunity creates one active Contract; an unauthorised service user cannot view the margin; a duplicate external event does not create a second payment. The name becomes a small requirement and the failing assertion tells an operator which promise was broken.

Keep one primary reason for failure per test. Shared setup is useful when it establishes a stable baseline, but each method should add only the data that makes its scenario distinct. Assert observable business state—records, statuses, relationships, returned values, published intent or explicit errors—not private implementation details. Tests coupled to helper methods and internal call order punish refactoring without protecting behaviour.

Make assertions precise enough to reject the wrong system

Assert that one invoice exists, not merely that the query returned something. Check the amount, owner, currency, source identifier and relationship that define correctness. For negative cases, prove both the error contract and the absence of side effects. A caught exception with no assertion can make a broken test pass; so can a query that silently finds an old fixture created by broad shared setup.

Salesforce recommends the Assert class and asks teams to cover positive, negative, bulk and single-record use cases. That is a useful minimum. Add invariants that survive implementation changes: totals balance, only permitted transitions occur, idempotency keys remain unique and partial failure returns record-level outcomes. If deliberately faulty logic could still satisfy the test, the assertion is too weak.

Test isolation is an architecture constraint

Apex tests from API version 24.0 onward are isolated from most existing org data by default. Salesforce recommends creating the required data in the test rather than relying on records that happen to exist. Preserve that boundary. SeeAllData=true turns a unit test into an environment-dependent experiment: administrators can change the result by editing production-like data, and the same commit can pass in one org but fail in another.

Isolation exposes hidden dependencies. Code that assumes a particular price book entry, custom setting row or integration user exists needs an explicit configuration boundary. Provide fixtures through small domain builders, dependency injection or metadata-aware adapters, and keep identifiers inside the test. Use SeeAllData only for a documented platform limitation, at the narrowest method scope, with an owner and an explanation of what makes the result deterministic.

Prove security in the user context that matters

A test running successfully as an administrator says little about a service agent, partner user or integration identity. Use System.runAs to exercise relevant user contexts and prove record visibility, object access and field access at the application boundary. Salesforce documents that runAs enforces the selected user's sharing rules and permissions in its block. Build personas from capabilities rather than copying whichever profile is convenient.

Test both authorised and unauthorised paths. A user who can update a Case may still be forbidden from reading a sensitive field or reassigning ownership. Assert the returned data and resulting state, not just the absence of an exception. Security tests should accompany the permission-set and sharing changes they protect, because a passing business-logic suite cannot reveal that a new grant exposed every record.

Exercise limits and collections as part of correctness

A single-record test can certify code that fails on the first data load. Salesforce testing guidance explicitly calls for bulk trigger coverage and recommends using at least 20 records. Choose volumes that expose collection mistakes, query amplification, duplicate handling and mixed outcomes. Assert every relevant record, not only the first item returned by an unordered query.

Use Test.startTest and Test.stopTest to isolate the operation under test and observe its governor consumption. Salesforce documents that startTest introduces a fresh limit context, while stopTest returns to the original context. Measure limits where regression risk is real, but avoid brittle assertions against an exact query count unless that count is the contract. A ceiling with intentional headroom is more useful than a test that fails after a harmless implementation change.

Bring asynchronous outcomes back into the assertion boundary

Queueable, batch, scheduled and future work can make a test appear complete before the business operation has happened. Salesforce collects asynchronous calls made after startTest and runs them synchronously when stopTest executes. Put the enqueue action inside that boundary, call stopTest, then assert the durable outcome. The enqueue itself is not success; the job's committed business state is the result.

Test failure and retry semantics deliberately: duplicate delivery, stale input, a downstream exception and a record that no longer qualifies when the worker runs. Keep chains shallow enough for deterministic unit tests, and move cross-system contracts into integration tests. A mock proves how Apex reacts to a defined response; it does not prove that the remote service still honours that response shape.

Operate the suite as a product

Salesforce Well-Architected guidance recommends a test pyramid: many fast isolated unit tests, fewer integration tests and selective end-to-end and performance tests. Run the appropriate layers automatically in CI and block promotion on failure. Track duration, intermittent failures, disabled tests and ownership. A flaky test is not harmless noise; it trains teams to rerun evidence until they receive the answer they wanted.

Review tests with production code. Ask which business risk the test controls, whether the assertions could detect a realistic defect, whether data and user context are explicit, and whether the failure message will help the next engineer. Remove duplication, but do not compress distinct risks into one enormous scenario. Coverage remains useful as a floor and a navigation aid. Release confidence comes from a suite that fails for the right reasons—and makes those reasons clear enough to act on.

Official references

Continue reading