newnimbus record
Managed packages

Record the package once, then test without the org

NPSP, Nebula, FSL and Vlocity are code you cannot read, step into or run without an org. nimbus record asks your org once what the package does and writes the answers to disk. Every run after that happens on your laptop, offline, in milliseconds, with nothing connected.

$nimbus record -o my-orgonce, against the org
$nimbus testevery run after, with no org and no flags
Recording

What nimbus record does

nimbus record is two passes in one. First it reads the package's real surface straight out of the org: every global class and signature, every constant, plus the labels, fields and objects your code touches. Then it runs your tests with the package's calls forwarded live to the org and writes down what came back, one cassette per test method.

bash
nimbus record -o my-org                    # the whole suite
nimbus record -o my-org DonationRollupTest # one class
nimbus record -o my-org --namespaces Nebula,npe01
nimbus record -o my-org --data             # settings + metadata rows too

Record

Once, against one org. Signatures and stable values land in stubs/, per-call answers in .nimbus/recordings/.

Commit

Stubs are plain Apex, cassettes are plain JSON. Check them into git and the whole team runs against the same answers.

Replay

nimbus test replays the cassettes. It needs no org, credentials or network, so CI can run it too.

The package stays a black box, but it now runs on your machine.

Recording writes stubs

What nimbus record writes to disk is a stub, a local implementation of a class Nimbus cannot otherwise see. Recording produces them for installed packages; you can also write them by hand for anything else. Three categories of code need them:

Managed packages

Nebula Logger, FSL, Vlocity, industry ISVs: closed source, installed in your org, source unavailable to Nimbus.

Unsupported APIs

ConnectApi, Metadata API, certain System methods: built in, but not implemented in Nimbus yet.

Custom mocking

Your own services that depend on external systems you want to mock for testing.

Example: Nebula Logger

apex
public class AccountService {
    public static void logAccountCreation(Account acc) {
        // Nebula Logger is a managed package - source is hidden
        Nebula.Logger.info('Created account: ' + acc.Name);
        Nebula.Logger.saveLog();
        // ← Can't execute - Nimbus can't see the code
    }
}

Without a stub, Nimbus fails when the test calls the Logger class.

User stubs

A stub is a user-provided implementation of a class Nimbus does not natively support. You implement just enough for your tests to run, then commit it to git so the team shares the same behavior.

✓

Testing does not wait on Nimbus

Your code is testable before Nimbus implements every API

✓

Works with closed packages

Auto-null fallback and namespace suppression handle packages whose source is not accessible

✓

Your code is the thing under test

The tests verify your business logic rather than the packages and APIs it depends on

✓

Small files

A stub holds only the methods your tests call

✓

Shared through git

Commit stubs to git so every team member shares the same behavior

What gets recorded

The first half of nimbus record is available on its own as nimbus stub pull: the package's real surface and real values, with no test run and nothing executed in the org:

bash
nimbus stub pull --org my-org --namespaces Nebula,npe01

Wrote stubs/labels/npe01.labels-meta.xml (14 labels: 14 new, 0 updated)
Wrote stubs/objects/Contact/fields/npe01__Private__c.field-meta.xml
Wrote stubs/Nebula/Logger.cls
Wrote stubs/Nebula/LogEntryEventBuilder.cls
Wrote stubs/npe4/Relationships_INST.cls
...
108 labels in 5 files, 162 fields, 2 objects, 17 classes (1 recorded constant), 0 skipped.
  • Classes: the Tooling API's SymbolTable exposes a package's exact global surface: every method overload, constructor, property, inner class, and enum, with the org's own signatures. Builder methods hand back constructed objects, so Nebula.Logger.info('x').addTag('y') chains work instead of NPE'ing.
  • Constant values: global constants have no value in a SymbolTable, so their actual values are read by running one short anonymous Apex script per class against the org.
  • Labels, fields, objects: custom labels from the only API that can read another package's labels, plus field and object schema for everything your project references.
  • Data, opt-in (--data): custom-setting rows become seed lines in a fenced block of nimbus.properties; custom-metadata records become ordinary stubs/customMetadata/ files. getInstance() and SOQL then return what the org returns.

Re-running is safe: anything your project defines, and anything already under stubs/, is left alone. Recorded stubs are plain files. Hand-edit them and commit them to git. See the full flag reference.

How stubs work

  1. create

    Create a stubs/ directory

    Add a stubs/ folder at 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.

    apex
    my-salesforce-project/
    ├── force-app/
    │   └── main/default/classes/
    ├── stubs/
    │   ├── Nebula/                                  (one folder per package)
    │   │   ├── Nebula.cls                           (Nebula.Logger, Nebula.LogEntryEventBuilder)
    │   │   └── objects/                             (namespaced custom objects)
    │   │       └── Nebula__LogEntryEvent__e/...
    │   ├── fflib/
    │   │   └── fflib.cls                            (fflib_SObjectDomain, fflib_Application, ...)
    │   └── ConnectApi/
    │       └── ConnectApi.cls                       (ConnectApi.FeedItem, etc.)
    └── sfdx-project.json

    To add another package later, create stubs/<NewPackage>/ next to the others.

  2. write

    Write minimal stubs

    Managed packages use namespaced classes like Nebula.Logger. The hand-written convention puts the namespace as the outer class and each package class as an inner class. Implement just the methods your tests call:

    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 error(String message) {
                System.debug('ERROR: ' + 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;
            }
        }
    }

    An equivalent layout is one file per class: stubs/Nebula/Logger.cls with public class Logger { ... } directly. The stub loader registers both the simple name and the Nebula. alias automatically. nimbus stub auto uses this layout; pick whichever fits your editing style.

  3. load

    Nimbus loads stubs automatically

    Stubs load as regular Apex classes, with no special namespace mapping. The outer class name matches the namespace, and inner classes match the package APIs:

    bash
    $ nimbus test AccountServiceTest
        [stubs] Loaded 1 class(es) from stubs/
    Parsing Apex classes...
    Running tests...
  4. run

    Your test runs with stubs

    When your code calls Nebula.Logger.info(), the interpreter resolves Nebula as a class and Logger as an inner class, finding the stub naturally:

    apex
    public class AccountServiceTest {
        @isTest
        static void testLogging() {
            // Calls Nebula.Logger - resolved from the stub
            Nebula.Logger.info('Created account');
            Nebula.Logger.saveLog();
            // ✓ Uses the stub, not the real Nebula Logger
            // ✓ Test completes in milliseconds
        }
    }

Stubs generated from your code Pro

Writing stubs by hand means walking every reference to find which methods get called, with which arguments, returning what type. nimbus stub auto does that walk. It scans the project AST, finds every reference to a class Nimbus cannot resolve, infers the surface from how your code uses it, and writes one .cls per class.

Workflow

bash
# Preview without writing — see what would be generated
nimbus stub auto --dry-run

# Generate (skips files that already exist)
nimbus stub auto

# Re-run after adding new code that exercises the package
nimbus stub auto --merge        # appends only new methods; keeps your edits
nimbus stub auto --force        # full rewrite (discards hand edits)

Or generate stubs after a successful test run:

bash
nimbus test --write-stubs
nimbus test --write-stubs --write-stubs-merge

What it captures

  • Method names and arities exactly as your code calls them
  • Argument types from declared local variables (String s = ...; X.foo(s) → foo(String arg0))
  • Return types from surrounding context: assignment LHS, return statements, casts, !/&&/|| (Boolean), 'msg: ' + X.bar() (String)
  • Generic types preserved: List<MyType> rows = X.fetch() → fetch() returns List<MyType>
  • Constructors with their arities

What it does not capture

  • Parameter names: call sites do not carry them. Expect arg0, arg1. The package's published surface is the source of truth if you care about names; hand-edit afterwards.
  • Method bodies: generated stubs increment callCount and append to a calls list so tests can assert on invocation count, but the return value is a type-default (null, 0, false, ''). Hand-edit the body if you need richer behavior.

Dynamic dispatch

Apex supports a few runtime-resolved patterns the static walker cannot see, chiefly Type.forName('Pkg.X').newInstance() (used by fflib mocks, Force-DI, at4dx) and Database.query(buildAtRuntime). When you run nimbus test --write-stubs, the test pass surfaces these to the cross-run registry alongside the static scan: any class name passed to Type.forName at runtime that doesn't resolve becomes a type-only stub. Standalone nimbus stub auto only sees the static surface, since no tests run.

Type-only stubs (no methods, no fields) let Type.forName('Pkg.X').newInstance() succeed at test time instead of NPE'ing on the null return. Hand-edit the surface, or call the class statically once so the next --merge picks up its methods.

Detecting and registering namespaces

Auto-stub needs to know which chain roots are managed-package namespaces and which are plain class names; otherwise it cannot tell Pkg.Logger.info() apart from OuterClass.Inner.method(). The signal is nimbus.stubs.namespaces in your nimbus.properties; without an entry there, auto-stub falls back to a flat type-only shell (stubs/Pkg.cls) that doesn't match the runtime resolution path.

The walker detects this and tells you. After every run, any chain root that looks namespace-shaped (used as Root.Class.something with Root unresolved) is surfaced as a suggestion with a copy-pasteable config line:

bash
→ 1 chain root looks like a managed-package namespace
    Hoplog               (6 chained reference sites)

  Adding it to nimbus.stubs.namespaces lets nimbus stub auto
  generate the correct namespaced layout (stubs/<Pkg>/<Class>.cls)
  instead of a flat type-only shell.

    nimbus.stubs.namespaces=Hoplog

  Or re-run with --update-config to write the line and regenerate.

Pass --update-config (or --write-stubs-update-config for the test-loop variant) to do both in one step. The flag merges the new entries into nimbus.properties, then re-walks with the augmented namespace set so the stubs land in the right shape:

bash
nimbus stub auto --update-config
# ✓ Wrote 1 new stub
#     stubs/Hoplog/Logger.cls
# ✓ Registered 1 namespace(s) in nimbus.properties
#     Hoplog               (6 chained reference sites)

Existing config lines and comments are preserved; only the nimbus.stubs.namespaces= line is touched (or appended if missing). Safe to combine with --merge and --dry-run.

Iterating with --merge

By default an existing stub is left alone, so hand edits are safe, but new usage in the project does not reach the stub. --merge parses the existing file, finds which methods, fields and constructors are already declared, and appends only the new ones. Existing method bodies, parameter names and custom additions are preserved.

bash
# Day 1: generate from scratch
nimbus stub auto

# You hand-edit the stub: real return value, meaningful arg names

# Day 30: project added Pkg.X.newMethod() in three new tests
nimbus stub auto --merge
# → appends newMethod, leaves your edits intact

Idempotent: running --merge twice in a row is a no-op when nothing's changed.

Two equivalent layouts

Nimbus accepts two equivalent on-disk layouts for namespaced stubs. nimbus stub auto writes one file per class:

bash
stubs/Nebula/Logger.cls            # public class Logger { ... }
stubs/Nebula/LogEntryEventBuilder.cls

The hand-written convention bundles everything into a single file with the namespace as the outer class:

bash
stubs/Nebula/Nebula.cls            # public class Nebula { class Logger { ... } class LogEntryEventBuilder { ... } }

Both resolve Nebula.Logger the same way at runtime: the per-class layout uses the stub loader's alias registration, the nested layout uses Apex's own inner-class semantics. --merge works against either.

Namespaced custom objects

Some managed packages ship custom objects with a namespace prefix, such as Nebula__LogEntryEvent__e or fflib__Setting__c. 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's schema synthesizer reads the object/field XML the same way it reads any custom object in force-app/. It 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.

Running without stubs

Stubs are not required for a first run. When Nimbus encounters a class it does not know (a managed package, an unsupported API), it logs a warning, returns null and continues. A test fails only if it asserts on the return value of the missing class.

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

The workflow is:

  1. Run nimbus test and see which tests pass and which fail
  2. For failing tests, check if the failure is a missing dependency
  3. Add a stub only for classes whose return values you need to control

Most logging and telemetry calls (Nebula Logger, for example) need no stub, because nothing asserts on them.

Suppressing closed namespaces

Managed package source is not visible: ApexClass.Body returns (hidden) for installed packages. nimbus stub pull recovers the global surface from the org's SymbolTable, but a package's non-global internals stay out of reach, and sometimes none of it is needed. For namespaces where specific return values do not matter, suppress them in nimbus.properties:

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

The auto-null fallback warns; nimbus.stubs.namespaces declares that the namespace is stubbed out on purpose, so no warning is printed and CI output stays clean. Stub classes under stubs/<Pkg>/ take precedence when you need real implementations.

Writing effective stubs

One folder per package

Put everything for a managed package under stubs/<Pkg>/: Apex classes plus any namespaced custom objects. Inside the package folder, the outer class name matches the namespace and inner classes match the API. For Nebula.Logger, create stubs/Nebula/Nebula.cls with Logger as an inner class. Nimbus resolves the class by that hierarchy.

Keep them minimal

Only implement the methods your tests actually call. If you're testing code that calls Logger.info() but never calls Logger.debug(), don't stub debug().

✓ Good
apex
public class Nebula {
    public class Logger {
        public static LogEntryEventBuilder info(String msg) {
            System.debug(msg);
            return new LogEntryEventBuilder();
        }
        public static void saveLog() { }
    }
    public class LogEntryEventBuilder { }
}

Match signatures exactly

Parameter names, return types, and access modifiers should match the real package. This prevents subtle bugs when your code expects a certain signature.

✓ Good
apex
// Match the real Nebula signature exactly
public static LogEntryEventBuilder info(String message)

// Not:
public static void log(String msg)

Use sensible defaults

Stubs should have behavior that makes sense for testing. Logging methods might print to System.debug. Data-fetching methods might return empty lists or test data.

✓ Good
apex
public class DataService {
    public static List<Record> getRecords(String query) {
        // Stub: return empty list for testing
        return new List<Record>();
    }
}

Commit to git

Stubs are small and belong in version control. Every team member and CI then run with the same test environment.

What recording covers

The pull covers everything with a stable answer: signatures, enums, constant values, labels, schema, custom-setting rows, custom-metadata records. What it can't cover is per-call behavior: what Nebula.Logger.getVersionNumber() returns for specific arguments. That is the recording half, and it is also available on its own once stubs exist:

bash
nimbus test --record -o my-org MyTestClass
# Re-records one class without re-pulling. Forwards managed-package static
# calls to the org and writes what they return to .nimbus/recordings/.

nimbus test MyTestClass
# Replays from the cassettes. No org connection.

nimbus test MyTestClass --no-replay
# Ignores the cassettes and runs the stub bodies instead.

Recording only intercepts calls into classes that came from stubs/, which is why nimbus record bundles the pull: on a project with an empty stubs/ directory there is nothing to intercept, and test --record alone would capture nothing. A call that was never recorded falls back to its stub body rather than failing, so adding cassettes to an existing suite does not break it.

Recorded calls really execute in the org, with whatever side effects they normally have. Point -o at a scratch or developer org, not at production.

Two limits. Static methods only: a local stub instance has no counterpart in the org to forward to, so instance methods keep their stub bodies. And callouts are not recorded: the platform refuses a callout in a test without Test.setMock, so a test that deploys already ships its own mocks and there is nothing to capture.

The pattern is the same as VCR (Ruby), WireMock (Java) and Polly (JavaScript). It complements stub pull: the pull gives the package's real surface and stable values in one command, and recording fills in argument-dependent behavior.

Record your packages in one command

nimbus record -o my-org once, then nimbus test from then on. There is no configuration, and hand-written stubs still work for anything you would rather control yourself.

Free

nimbus record against any org you can authenticate to, and hand-written stubs for anything else. Both land in stubs/ and load as regular Apex classes.

Read the docs

Pro

Pro

nimbus stub auto walks every reference to a class Nimbus can't resolve and writes one .cls per class, with methods, arities and return types inferred from how your code uses them. Re-run with --merge as the codebase changes; hand edits stay intact.

See Pro features