Apex tests in your pipeline
without a Salesforce org
A Java team has Codecov badges. A Python team has pytest publishing JUnit XML to every PR. Nimbus gives an Apex team the same outputs, with no Salesforce org anywhere in the pipeline.
For a working GitHub Actions + GitLab walkthrough, see Apex CI without a connected org.
What an org-based pipeline needs
A Salesforce CI pipeline that uses an org is slow, and it depends on a JWT certificate, a connected DevHub, an available scratch org from a finite pool, and a network connection. Any one of those can fail, and when one does the cause is rarely obvious.
Every CI run uses a scratch org slot, and every scratch org expires. When the pool runs out during a release, CI stops, and the team pushes to main hoping the tests pass in production.
Org-based CI pipeline
# What you're replacing
steps:
- name: Authenticate to DevHub
# Requires: JWT cert in secrets, connected app
run: sf org login jwt --client-id $CLIENT_ID ...
- name: Create scratch org
# Burns a slot from your finite pool
# Fails silently if pool is exhausted
run: sf org create scratch --definition-file ...
- name: Deploy source
# 4-8 minutes, every run
run: sf project deploy start ...
- name: Run tests
# Another 3-5 minutes
run: sf apex run test ...
- name: Delete scratch org
# If the above crashed, this doesn't run
run: sf org delete scratch ...Nimbus CI pipeline
# What you get instead
steps:
- uses: actions/checkout@v4
- name: Install Nimbus
run: curl -fsSL https://install.testnimbus.dev | sh
- name: Run tests
run: nimbus test
--coverage --coverage-report coverage.xml
--results-xml results.xml
env:
NIMBUS_LICENSE_KEY: ${{ secrets.NIMBUS_LICENSE_KEY }}
NIMBUS_PROFILE: ciWhat Nimbus produces
Nimbus writes the standard CI formats, the same ones your Java, Python and Go pipelines produce. GitHub Actions, SonarQube, Codecov and Jenkins read them without custom integration work.
The XML and HTML formats are Pro. Running the suite on a CI runner is part of the Team plan, which includes a CI runner license with its own machine pool.
Test results for CI dashboards
PR annotations, test trend tracking, failure history. Works with GitHub Actions, GitLab CI, Jenkins, CircleCI.
--results-xml results.xmlCoverage for SonarQube & Codecov
The standard coverage format. SonarQube, Codecov and GitHub Actions coverage gates read it directly.
--coverage-report coverage.xmlVisual coverage report
Browse coverage line-by-line in your browser. Share with the team as a CI artifact.
--coverage-report coverage.htmlCustom tooling & dashboards
Machine-readable coverage data. Build your own dashboards or integrate with internal tooling.
--coverage-report coverage.jsonGitHub Actions
The minimal setup installs Nimbus and runs the tests. There are no service containers and no external dependencies. Nimbus starts its own embedded PostgreSQL and tears it down when the job finishes.
Add coverage and JUnit XML output to get PR annotations, coverage trends and SonarQube or Codecov integration in the same job.
On large projects, a Postgres service container with DATABASE_URL removes the embedded database startup time and lets you pre-warm the schema.
name: Apex Tests
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Nimbus
run: curl -fsSL https://install.testnimbus.dev | sh
- name: Run tests
run: |
nimbus test \
--coverage --coverage-report coverage.xml \
--results-xml results.xml
env:
NIMBUS_LICENSE_KEY: ${{ secrets.NIMBUS_LICENSE_KEY }}
NIMBUS_PROFILE: ci
- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: results.xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}With external Postgres (faster for large projects)
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: nimbus_ci
POSTGRES_USER: nimbus
POSTGRES_PASSWORD: nimbus
ports: ['5432:5432']
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Install Nimbus
run: curl -fsSL https://install.testnimbus.dev | sh
- name: Run tests
run: nimbus test --coverage --coverage-report coverage.xml
env:
DATABASE_URL: postgres://nimbus:nimbus@localhost:5432/nimbus_ci
NIMBUS_LICENSE_KEY: ${{ secrets.NIMBUS_LICENSE_KEY }}
NIMBUS_PROFILE: ciGitLab CI
GitLab CI supports JUnit XML natively via artifacts:reports:junit, so test results appear in merge request widgets without a plugin.
Cobertura XML integrates with GitLab's built-in coverage visualization, showing diff coverage directly on merge request diffs.
apex-tests:
image: ubuntu:22.04
script:
- curl -fsSL https://install.testnimbus.dev | sh
- nimbus test
--coverage --coverage-report coverage.xml
--results-xml results.xml
variables:
NIMBUS_LICENSE_KEY: $NIMBUS_LICENSE_KEY
NIMBUS_PROFILE: ci
artifacts:
when: always
reports:
junit: results.xml
coverage_report:
coverage_format: cobertura
path: coverage.xmlSonarQube & SonarCloud
SonarQube and SonarCloud consume Cobertura XML for coverage and can parse JUnit XML for test result history. Point both at Nimbus output and the Apex project gets the same quality gate as the rest of the codebase.
Configure sonar.apex.coverage.reportPaths in your sonar-project.properties. The Cobertura report maps line coverage to each class file so SonarQube can display coverage inline.
# sonar-project.properties
sonar.projectKey=my-salesforce-project
sonar.sources=force-app/main/default/classes
sonar.tests=force-app/main/default/classes
sonar.test.inclusions=**/*Test.cls
# Point at Nimbus Cobertura output
sonar.apex.coverage.reportPaths=coverage.xml
# JUnit results for test history
sonar.junit.reportPaths=results.xml# In your CI pipeline
- name: Run tests
run: nimbus test
--coverage --coverage-report coverage.xml
--results-xml results.xml
env:
NIMBUS_PROFILE: ci
- name: SonarQube scan
uses: SonarSource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}The CI profile
Nimbus supports Quarkus-style configuration profiles. Commit a nimbus.properties file to your repo and define CI-specific overrides with the %ci. prefix. Set NIMBUS_PROFILE=ci in your pipeline and the overrides apply.
The CI environment then gets strict governor limit enforcement, no browser opening, no tracing output and an external database, all defined once in source control rather than spread across pipeline environment variables.
Full configuration reference →# nimbus.properties - commit to your repo
# Defaults: relaxed for local development
nimbus.governor.mode=warn
nimbus.trace.enabled=true
nimbus.devui.open-browser=true
# CI overrides - activated by NIMBUS_PROFILE=ci
%ci.nimbus.governor.mode=strict
%ci.nimbus.test.parallel=4
%ci.nimbus.trace.enabled=false
%ci.nimbus.devui.open-browser=false
# Use DATABASE_URL from environment in CI
%ci.nimbus.db.provider=external
%ci.nimbus.db.url=${DATABASE_URL}# Activate in CI - one environment variable
NIMBUS_PROFILE=ci nimbus test
# Or pass the flag explicitly
nimbus test --profile ciParallel sharding
Split your test suite across multiple CI jobs with --shard N/M. Job N runs only its assigned slice of test classes. All jobs run concurrently in a matrix, cutting wall-clock time proportionally.
Classes are distributed by modulo assignment rather than contiguous slicing, so uneven class durations balance across shards. All methods within a class stay together, so @testSetup runs correctly.
When combined with --results-xml, each shard's JUnit XML includes a shard="N/M" attribute so CI aggregators can merge results across shards.
name: Apex Tests (sharded)
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- name: Install Nimbus
run: curl -fsSL https://install.testnimbus.dev | sh
- name: Run tests (shard ${{ matrix.shard }}/4)
run: |
nimbus test \
--shard ${{ matrix.shard }}/4 \
--results-xml results-${{ matrix.shard }}.xml
env:
NIMBUS_LICENSE_KEY: ${{ secrets.NIMBUS_LICENSE_KEY }}
NIMBUS_PROFILE: ci
- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: results-${{ matrix.shard }}.xmlCaching for faster runs
Nimbus caches parsed ASTs in .nimbus/cache/. On a project with hundreds of Apex classes, this can reduce startup time from several seconds to under a second on subsequent runs.
Cache the .nimbus/cache/ directory between CI runs, keyed on your Apex source files. The binary download and embedded PostgreSQL runtime can also be cached to save a few more seconds.
- name: Cache Nimbus
uses: actions/cache@v4
with:
path: |
~/.nimbus
.nimbus/cache
key: nimbus-${{ hashFiles('force-app/**/*.cls', 'force-app/**/*.trigger') }}
restore-keys: nimbus-
- name: Cache binary
uses: actions/cache@v4
with:
path: /usr/local/bin/nimbus
key: nimbus-bin-${{ runner.os }}-v1Apex CI without an org
There is no org, no JWT and no scratch org pool. JUnit XML and Cobertura coverage go into the same tools your other languages already use.