Docs/Start

Start

Install Nimbus, prepare a Salesforce project, and get the first local test passing.

Getting Started

Installation

Single static binary. No JVM, no Node, no Docker. macOS, Linux, and Windows (native, no WSL) are all supported.

bash
# macOS / Linux
curl -fsSL https://install.testnimbus.dev | sh

# Windows — works in cmd.exe, Windows Terminal, or any shell
powershell -c "irm https://install.testnimbus.dev/win | iex"

# Windows alternative: Scoop
scoop bucket add nimbus https://github.com/nimbus-solution/scoop-nimbus
scoop install nimbus

# Verify
nimbus --version

The installer drops the binary in /usr/local/bin, or ~/.local/bin when that is not writable (macOS/Linux), or %LOCALAPPDATA%\Programs\Nimbus (Windows), and puts that directory on your PATH if it is not already there: one export line in your shell profile on macOS/Linux, the user PATH on Windows. INSTALL_DIR chooses the directory, NIMBUS_VERSION pins a release, and NIMBUS_NO_MODIFY_PATH=1 leaves your dotfiles alone and prints the line to add instead. Re-run any time to upgrade. See the Quickstart guide for platform-specific notes.

Quick Start

From a fresh install to a passing test in five commands:

bash
# 1. Initialize Nimbus in your SFDX project root
cd path/to/your/sfdx-project
nimbus init

# 2. Verify setup
nimbus doctor

# 3. Run all tests
nimbus test

# 4. Run a specific class or method
nimbus test AccountTriggerTest
nimbus test AccountTriggerTest.testInsertCreatesContact

# 5. Run with coverage
nimbus test --coverage

The first run parses your Apex into an AST cache — a few seconds. Subsequent runs are milliseconds. See the full Quickstart guide for the starter project, Dev UI, and org sync steps.

Project Setup

Nimbus stores its state in a .nimbus/ directory at your project root:

bash
.nimbus/
├── db/          # Embedded PostgreSQL data directory
├── schemas/     # Describes pulled by nimbus sync
├── cache/       # Parsed AST cache
├── traces/      # Execution traces (when --trace is used)
├── recordings/  # Managed-package recordings (nimbus test --record)
├── evidence/    # Per-line run evidence (nimbus test --evidence)
└── history/     # Run history, impact map, test durations

You do not have to ignore this by hand. The first time the runtime starts in a git repository it appends a .nimbus/ line to the project's .gitignore, creating the file if there is none. It only ever appends: an existing line is never rewritten, nothing else in the file is touched, and a file git already tracks stays tracked — git ignores nothing it is already following, so a repository that has already committed a .nimbus/ tree has to be untracked deliberately, with git rm -r --cached .nimbus.

bash
# .gitignore — written on first run
.nimbus/

If you would rather commit some of it — traces are worth sharing with a team — narrow the line yourself. Nimbus will not widen it again.

Disk footprint

A project that has run its suite once carries roughly 60–90 MB in .nimbus/, and almost all of it is db/ — a live PostgreSQL data directory, which is what a real database costs on disk whatever is in it. On the demo project the split measures 59 MB db/, 7 MB schemas/, under 1 MB cache/, and tens of kilobytes of JSON. It is kept between runs on purpose: the next run starts on tables that already exist instead of rebuilding all of them.

The PostgreSQL binaries are not part of that. They are extracted once per machine to ~/.nimbus/pg/ (about 190 MB) and shared by every project, so a second project costs its own data directory and nothing more. Nimbus keeps its downloaded releases in ~/.nimbus/bin/.

nimbus reset deletes the whole per-project .nimbus/ and gives that space back; nimbus data reset empties the tables without touching the directory, which is faster but reclaims little — PostgreSQL does not shrink its files when rows go away.

User Stubs / Managed Packages

Sometimes Nimbus doesn't natively support a class or API: managed packages (Nebula Logger, FSL, Vlocity), unsupported built-in APIs (ConnectApi, Metadata), or custom code you want to mock. Use stubs to provide your own implementation.

What are stubs?

Stubs are user-provided Apex classes that implement APIs Nimbus doesn't know about. Instead of blocking on missing code, you implement just enough for your tests to run - then commit stubs to git so your team shares the same behavior.

How to use stubs

Create a stubs/ directory in your project root, with one folder per managed package. Each package folder holds everything for that package — Apex classes and namespaced custom objects/fields. Nimbus loads it all with the lowest priority; anything in force-app/ with the same name takes precedence.

bash
my-salesforce-project/
├── force-app/
├── stubs/                                      # User-provided implementations
│   ├── Nebula/                                # one folder per package
│   │   ├── Nebula.cls                         # Apex surface (Nebula.Logger, ...)
│   │   └── objects/                           # namespaced custom objects
│   │       └── Nebula__LogEntryEvent__e/...
│   └── ConnectApi/
│       └── ConnectApi.cls                     # ConnectApi.FeedItem, etc.
└── sfdx-project.json

Adding another package later is just stubs/<NewPackage>/ next to the others. Surfaces stay cleanly separated, no cross-package collisions.

Writing a stub: example approach

Managed packages use namespaced classes like Nebula.Logger. In Apex, the namespace becomes the outer class and the package class becomes an inner class. Create stubs/Nebula.cls:

apex
// stubs/Nebula/Nebula.cls
public class Nebula {

    public class Logger {

        public static LogEntryEventBuilder info(String message) {
            System.debug('INFO: ' + message);
            return new LogEntryEventBuilder();
        }

        public static LogEntryEventBuilder info(String message, SObject record) {
            System.debug('INFO: ' + message);
            return new LogEntryEventBuilder();
        }

        public static LogEntryEventBuilder info(String message, List<SObject> records) {
            System.debug('INFO: ' + message);
            return new LogEntryEventBuilder();
        }

        public static LogEntryEventBuilder info(String message, Id recordId) {
            System.debug('INFO: ' + message);
            return new LogEntryEventBuilder();
        }

        public static LogEntryEventBuilder info(String message, List<Id> recordIds) {
            System.debug('INFO: ' + message);
            return new LogEntryEventBuilder();
        }

        public static LogEntryEventBuilder error(String message) {
            System.debug('ERROR: ' + message);
            return new LogEntryEventBuilder();
        }

        public static LogEntryEventBuilder error(String message, SObject record) {
            System.debug('ERROR: ' + message);
            return new LogEntryEventBuilder();
        }

        public static LogEntryEventBuilder error(String message, List<SObject> records) {
            System.debug('ERROR: ' + message);
            return new LogEntryEventBuilder();
        }

        public static LogEntryEventBuilder error(String message, Id recordId) {
            System.debug('ERROR: ' + message);
            return new LogEntryEventBuilder();
        }

        public static LogEntryEventBuilder error(String message, List<Id> recordIds) {
            System.debug('ERROR: ' + message);
            return new LogEntryEventBuilder();
        }

        public static LogEntryEventBuilder warn(String message) {
            System.debug('WARN: ' + message);
            return new LogEntryEventBuilder();
        }

        public static LogEntryEventBuilder warn(String message, SObject record) {
            System.debug('WARN: ' + message);
            return new LogEntryEventBuilder();
        }

        public static LogEntryEventBuilder warn(String message, List<SObject> records) {
            System.debug('WARN: ' + message);
            return new LogEntryEventBuilder();
        }

        public static LogEntryEventBuilder warn(String message, Id recordId) {
            System.debug('WARN: ' + message);
            return new LogEntryEventBuilder();
        }

        public static LogEntryEventBuilder warn(String message, List<Id> recordIds) {
            System.debug('WARN: ' + message);
            return new LogEntryEventBuilder();
        }

        public static void saveLog() {
            // Stub: no-op
        }
    }

    public class LogEntryEventBuilder {

        public LogEntryEventBuilder addTag(String tag) {
            return this;
        }

        public LogEntryEventBuilder setRecord(Object record) {
            return this;
        }

        public LogEntryEventBuilder setException(Exception ex) {
            return this;
        }

        public LogEntryEventBuilder setExceptionDetails(Exception ex) {
            return this;
        }
    }
}

How stubs are resolved

Stubs are loaded as regular Apex classes — no special namespace mapping. When your production code calls Nebula.Logger.info(), the interpreter resolves Nebula as a class and Logger as an inner class, finding the stub naturally.

If a test calls a managed package class that doesn't have a stub, Nimbus degrades gracefully: it logs a warning and returns null instead of failing the test. Stubs are opt-in — you only need one when you require specific return values or side effects.

Namespaced custom objects

Some managed packages ship custom objects with a namespace prefix (Nebula__LogEntryEvent__e, fflib__Setting__c, etc.). If your tests do DML or SOQL against them, drop the schema XML inside the same package folder under objects/:

bash
stubs/
└── Hoplog/
    ├── Hoplog.cls                              # Apex surface (Hoplog.Logger, ...)
    └── objects/
        └── Hoplog__LogEntry__c/                # namespaced custom object
            ├── Hoplog__LogEntry__c.object-meta.xml
            └── fields/
                ├── Hoplog__Severity__c.field-meta.xml
                ├── Hoplog__EventType__c.field-meta.xml
                └── Hoplog__Message__c.field-meta.xml

Nimbus reads the object/field XML the same way it reads any custom object in force-app/ — creates a matching table in the embedded Postgres so insert new Hoplog__LogEntry__c(...) and SELECT ... FROM Hoplog__LogEntry__c work end-to-end without the package being installed in any org. By convention, capitalize the namespace folder and class name; Apex itself is case-insensitive (so hoplog.Logger and Hoplog.Logger both resolve), but consistent capitalization is clearer on grep and in code review.

Namespaced custom labels

Managed packages ship custom labels too, and Apex reads them as Label.npe03.RecurringDonationStageName — the dotted form is the only spelling the platform compiles for a packaged label. Because a retrieved CustomLabels file carries no namespace element, Nimbus puts the namespace in the file name:

bash
stubs/
└── labels/
    ├── CustomLabels.labels-meta.xml   # unnamespaced labels → Label.MyLabel
    ├── npe03.labels-meta.xml          # → Label.npe03.<Name>
    └── npo02.labels-meta.xml          # → Label.npo02.<Name>

The file contents are ordinary CustomLabels metadata — the same XML sf project retrieve writes:

xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomLabels xmlns="http://soap.sforce.com/2006/04/metadata">
    <labels>
        <fullName>RecurringDonationStageName</fullName>
        <language>en_US</language>
        <protected>false</protected>
        <shortDescription>RecurringDonationStageName</shortDescription>
        <value>Pledged</value>
    </labels>
</CustomLabels>

Stub labels only fill gaps: a label the project itself defines always wins the unnamespaced lookup. A label that resolves nowhere still degrades to an empty string with a warning naming the ns.Name it could not find, so adding label stubs never introduces a new hard failure.

Write them by hand, scaffold one with nimbus stub label, or pull an entire package's labels out of an org that has it installed with nimbus stub pull.

Equivalent layout: one file per class

The nested layout above (one file per package, classes as inner classes) co-locates the full Apex surface alongside any namespaced custom objects. An equivalent layout is one file per class — stubs/<ns>/<Class>.clswith public class <Class> { ... } directly, no outer wrapper. The stub loader registers <ns>.<Class> as an alias automatically, so both mp.Logger.info() and Logger.info() resolve to the same stub. Both layouts are first-class — pick whichever fits your editing style. nimbus stub add and nimbus stub auto (Pro) write the per-class layout; the nested layout is the hand-written convention.

Auto-null fallback behavior

When Nimbus encounters an unresolved class, it emits a warning and continues:

bash
[warn] nebula.logger.Logger not found - returning null (add a stub to control behavior)
[warn] fsl.FieldServiceAPI not found - returning null

Tests that don't assert on the return value of those calls will pass. Tests that do assert on the value will fail with a clear message pointing at the missing class. This means you can get your first green run without writing any stubs - add them incrementally as you need control over return values.

Suppressing entire namespaces

For closed managed packages where you don't need specific return values - and can't see the source anyway - the auto-null fallback handles most cases silently. If you want to explicitly declare a namespace as intentionally unsupported (suppressing the warnings), use nimbus.properties:

bash
# nimbus.properties
# Comma-separated list of namespaces to treat as opaque - no warnings, methods
# return null. Matching is case-insensitive.
nimbus.stubs.namespaces=Nebula,fflib,fsl

This is especially useful in CI where you want a clean output: warnings from auto-null suppressed, behavior consistent, no stub files cluttering the repo for namespaces your tests don't assert on.

Best practices

  • One folder per package: Drop everything for a managed package under stubs/<Pkg>/ — Apex classes plus any namespaced custom objects. Inside, the outer class name matches the namespace and inner classes match the API
  • Keep stubs minimal: Only implement methods your tests actually call
  • Match signatures: Parameters, return types, and access modifiers should match the real API
  • Use sensible defaults: Return empty lists, null, or debug output to help test behavior
  • Commit to git: Stubs are small and help team members reproduce tests locally without external dependencies

For truly closed packages (no public source), the auto-null fallback and nimbus.properties noop declarations are your tools - or the future recording mode.

Roadmap: recording mode

The only fully-automated path for truly closed managed packages - where no source exists and no stub registry can help - is recording mode. Run nimbus test --record -o my-org once against a real org; Nimbus captures the actual runtime return values. All future local test runs replay those values without org access. Same pattern as VCR (Ruby), WireMock (Java), and Polly (JavaScript).