User stubs

Extending Nimbus with stubs

Managed packages, unsupported APIs, custom implementations - provide stubs for anything Nimbus doesn't natively support yet. Test your code without being blocked.

The problem

Nimbus implements a large subset of Apex, but not everything yet. Three categories of code block test execution:

Managed packages

Nebula Logger, FSL, Vlocity, industry ISVs - closed-source, installed in your org, but 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.

The solution: user stubs

A stub is a user-provided implementation of any class Nimbus doesn't natively support. Instead of blocking on missing code, you implement just enough for your tests to run - then commit it to git so your team shares the same behavior.

Unblock testing

Test your code now, without waiting for Nimbus to implement every API

Works with closed packages

Auto-null fallback and namespace suppression handle packages where source isn't accessible

Test your code, not dependencies

Focus on verifying your business logic, not the packages or APIs you depend on

Lightweight and maintainable

Stubs are simple - just the methods your tests actually call

Team-shareable

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

How stubs work

1

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

Adding another package later is just stubs/<NewPackage>/ next to the others.

2

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

Nimbus loads stubs automatically

Stubs are loaded as regular Apex classes - no special namespace mapping needed. 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

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
    }
}

Auto-generate stubs from your code Pro

Writing stubs by hand is straightforward, but tedious — you have to walk every reference to figure out which methods get called, with which arguments, returning what type. nimbus stub auto does that walk for you. It scans the project AST, finds every reference to a class Nimbus can't resolve, infers the surface from how your code actually 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 fold it into the test loop — generate stubs after a successful 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 doesn't

  • Parameter names — call sites don't 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 can't 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 vs. plain class names — otherwise it can't 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 shot — 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

The default behavior is conservative: if a stub already exists, it's left alone — your hand edits are safe. But that means new project usage doesn't trickle into the stub. --merge is the answer: it parses the existing file, identifies which methods/fields/ctors are already declared, and appends only the new ones. Existing method bodies, parameter names, even custom additions you wrote — all 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 layouts, both work

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 automatic alias registration; the nested layout uses Apex's own inner-class semantics. Pick whichever you prefer; --merge works against either.

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's schema synthesizer 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.

Zero-stub start

You don't need stubs to get your first green run. When Nimbus encounters a class it doesn't know - a managed package, an unsupported API, anything - it degrades gracefully: logs a warning, returns null, and continues. Tests only fail if they assert on the return value of that missing class.

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

This means your workflow is:

  1. Run nimbus test - 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, etc.) require no stub at all - you don't assert on them.

Suppressing closed namespaces

Managed package source code isn't visible - ApexClass.Body returns (hidden) for installed packages. You can't introspect method signatures, so you can't always write complete stubs. For namespaces where you don't need specific return values, suppress them explicitly 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

Unlike the auto-null fallback (which warns), nimbus.stubs.namespaces is a deliberate declaration: this namespace is intentionally stubbed out, nothing to act on. Keeps CI output clean. Stub classes under stubs/<Pkg>/ always take precedence when you need real implementations.

Writing effective stubs

One folder per package

Drop 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. The class hierarchy is the contract.

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 should be committed to version control. This ensures every team member has the same test environment - no surprises when tests run differently locally vs. in CI.

Roadmap: recording mode

nimbus stub auto covers the static side: surface inferred from how your code calls the package. The remaining gap is runtime behavior — what does Nebula.Logger.info() actually return when called with these arguments against a real org? For tests that depend on specific runtime return values (not just call counts), recording mode is the answer. Run against a real org once to capture actual return values; all future test runs replay locally without touching the org.

bash
# Planned - not yet implemented
nimbus test --record -o my-org MyTestClass
# Captures real return values from managed packages and callouts
# Saves to .nimbus/recordings/ - commit to git for team sharing
# Subsequent runs replay without any org connection

Same pattern as VCR (Ruby), WireMock (Java), and Polly (JavaScript) — complementary to auto-stub: auto-stub gives you a working type surface in seconds, recording fills in real return values when you need them.

Start stubbing in minutes

Create a stubs/<Pkg>/ folder, drop in an Apex class (and namespaced objects, if you need them), and run your tests. No configuration required.

Free

Write your own stubs for any code Nimbus doesn't support yet. Place them in stubs/ - Nimbus loads them as regular Apex classes automatically.

Read the docs

Pro

Pro

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

See Pro features