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 verifies a SHA256 checksum and drops the binary in ~/.local/bin (macOS/Linux) or %LOCALAPPDATA%\Programs\Nimbus (Windows). 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
├── cache/       # Parsed AST cache
└── traces/      # Execution traces (when --trace is used)

Add .nimbus/db/ and .nimbus/cache/ to your .gitignore. Traces can optionally be committed for sharing with your team.

bash
# .gitignore
.nimbus/db/
.nimbus/cache/

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.

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).