From Flaky to Fast: Engineering Reliable Apex Tests for CI/CD on Salesforce

Engineering patterns to make Apex tests deterministic and fast for Salesforce CI/CD: factories, @testSetup, async control, and reliable assertions.

Salesforce engineering teams often hit a wall when CI/CD pipelines become noisy: tests pass locally, fail in packaging, or time out in scratch orgs. The root cause is rarely “Salesforce being random”-it’s usually test design. In this second post of our engineering series, we’ll focus on practical patterns that make Apex test classes reliable, deterministic, and fast enough for continuous delivery.

## Why Apex Tests Become Flaky in Pipelines

Flakiness is typically introduced by one of three factors:

- **Hidden org dependencies**: tests that assume certain metadata, Profiles, queues, or existing records.

- **Non-deterministic data**: reliance on `SeeAllData=true`, ordering assumptions, or unpredictable async timing.

- **Poor isolation**: tests that mix responsibilities (setup + business logic + assertions) and are hard to reason about.

CI environments amplify these issues because scratch orgs and packaging orgs are intentionally minimal and may execute tests under different timing and load conditions.

## Engineering Patterns for Deterministic Apex Tests

### Prefer Purpose-Built Test Data Factories

A test data factory centralizes record creation and makes intent obvious. Keep factories:

- **Small and composable** (e.g., `makeAccount()`, `makeOpportunity(accountId)`)

- **Explicit** about required fields and relationships

- **Stable** in defaults (avoid random values unless you seed them deterministically)

This reduces drift across hundreds of tests and prevents “fix it in one class, break it in another” behavior.

### Use `@testSetup` Strategically

`@testSetup` is ideal for stable reference data used across methods in a test class. Use it when:

- multiple test methods need the same baseline graph of data

- setup time is significant

Avoid putting highly variable data (edge cases, validation failures, special permissions) into `@testSetup`. Keep those within each test method so the scenario is clear.

### Boundaries, Not Internals: Assert on Outcomes

Reliable tests validate behavior, not implementation details. Instead of asserting exact queries or private-state transitions, assert:

- record state changes (fields updated, records created)

- side effects (Platform Events published, Tasks created)

- errors for invalid input (expected exceptions)

This makes refactors safer and lowers maintenance cost.

## Designing for Async and Limits

### Control Async with `Test.startTest()` / `Test.stopTest()`

Async execution is a common pipeline failure point. Always wrap the code under test:

- enqueue operations inside `Test.startTest()`

- assert results after `Test.stopTest()` so queued work completes

When testing Queueables or Futures, validate both that the job is invoked and that the final data state is correct.

### Keep Tests Lean to Avoid Timeouts

CI failures often come from cumulative slowness across the suite. Speed up by:

- minimizing record volume (create the smallest dataset that proves the behavior)

- avoiding unnecessary triggers/flows in tests via design patterns (e.g., bypass flags only where appropriate)

- focusing on one behavior per test method

## Where AI Fits: Coverage That’s Also Correct

Code coverage alone doesn’t guarantee quality, but high-quality tests do require time. Tools like **Apex AI** help engineering teams generate Apex test classes that follow best practices-creating isolated data, exercising success and failure paths, and producing meaningful assertions-so CI/CD pipelines stay green without sacrificing rigor.

## Conclusion

Reliable Apex tests are engineered, not improvised. Standardize data creation, isolate scenarios, control async execution, and assert on outcomes. If you want to accelerate this work across your Salesforce codebase, explore how Apex AI can generate robust Apex test classes that are built for CI/CD from day one.