renmy runtime is a local Salesforce Apex runtime: it executes Apex, SOQL, DML, triggers and Salesforce Flows on your machine against an embedded PostgreSQL, with no org. A managed package is the one thing it cannot see, because Salesforce hides a package’s class bodies even from the org that installed it. There are two answers. Write a small stub in stubs/ for the methods your tests call, or run renmy record -o <org> once: it reads the package’s real surface and values out of a scratch or developer org into stubs/, forwards your tests’ package calls to that org and writes the answers to .nimbus/recordings/. Commit both, and every run after that, locally and in CI, needs no org.
Nimbus was renamed renmy runtime in October 2026. Same tool, same binary; the record of the rename is here.
Who this is for. ISV developers who build managed packages, and the subscribers who call them, in particular nonprofit developers writing Apex tests against NPSP. You do not need to know what a local runtime is.
Why a managed package breaks local Apex tests
When a Java or JavaScript developer presses “run test”, a runner on their laptop loads their code and every dependency and prints the result in milliseconds. The dependency need not ship source; a jar or an npm package is enough, because the local runtime can execute it.
Salesforce never had the laptop half of that picture. Apex compiles and runs inside an org, so “run test” has meant deploy, wait for the queue, run, read. renmy runtime adds the laptop half: a real Apex interpreter, SOQL and DML against an embedded PostgreSQL, triggers and flows, governor limits enforced, running your tests from force-app/ in seconds rather than minutes (method and numbers; the idea from zero).
A managed package is where the analogy breaks. There is no jar. Query ApexClass.Body for an installed package and you get the string (hidden). Your force-app/ holds the code that calls Nebula.Logger.info() and queries npe01__Private__c on Contact, but nothing that defines either. A package contributes three kinds of things your code depends on, and a runtime with only your source has none of them:
- Classes. The global surface you call: static methods, builders, enums, constants.
- Schema. Namespaced objects and fields, such as
npe01__Private__corNebula__LogEntryEvent__e. - Values. Custom labels, custom-setting rows and custom-metadata records your code reads.
What happens if you run the test anyway
When renmy runtime meets a class it does not know, it logs a warning, returns null and continues:
$ renmy test "*"
[warn] nebula.logger.Logger not found - returning null (add a stub to control behavior)
[warn] fsl.FieldServiceAPI not found - returning nullA test fails only if it asserts on the return value of the missing class. Most logging and telemetry calls need no stub, because nothing asserts on them. So the first move is renmy test "*", then separate missing-dependency failures from your own. For namespaces where return values never matter, say so in renmy.properties and the warnings leave your CI output:
# renmy.properties
renmy.stubs.namespaces=Nebula,fflib,fslThat covers classes for code that merely logs. It does not cover schema: npe01__Private__c exists because the package put it in your org, not in anybody’s force-app/, so a test that touches it needs the field definition from somewhere. Recording writes it.
Writing a managed package stub by hand
A stub is a local implementation of a class renmy runtime cannot otherwise see: a managed-package class, or an API renmy runtime does not implement yet (ConnectApi, Metadata API, certain System methods). You implement just enough for your tests to run and commit it. Stubs live in stubs/ at the project root, one folder per package, holding that package’s Apex classes and its namespaced objects. renmy runtime loads them with the lowest priority; anything in force-app/ with the same name wins.
my-salesforce-project/
├── force-app/
│ └── main/default/classes/
├── stubs/
│ ├── Nebula/
│ │ ├── Nebula.cls (Nebula.Logger, Nebula.LogEntryEventBuilder)
│ │ └── objects/
│ │ └── Nebula__LogEntryEvent__e/...
│ └── ConnectApi/
│ └── ConnectApi.cls
└── sfdx-project.jsonThe hand-written convention puts the namespace as the outer class and each package class as an inner class, with only the methods your tests call:
// 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 void saveLog() {
// Stub: no-op
}
}
public class LogEntryEventBuilder {
public LogEntryEventBuilder addTag(String tag) { return this; }
}
}One file per class, stubs/Nebula/Logger.cls, is an equivalent layout. Either way, when your code calls Nebula.Logger.info(), the interpreter finds the stub:
$ renmy test AccountServiceTest
[stubs] Loaded 1 class(es) from stubs/
Parsing Apex classes...
Running tests...Namespaced objects work the same way: drop the object and field XML under stubs/<Pkg>/objects/, and the schema synthesiser creates the matching table in the embedded PostgreSQL, so insert and SELECT against Nebula__LogEntryEvent__e work end to end.
For a logger this is ten lines. For NPSP it is dozens of global classes, hundreds of namespaced fields, labels and custom settings. Nobody should type that by hand; recording exists for that case.
Recording a managed package once with renmy record
renmy 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.
renmy record -o my-org # the whole suite
renmy record -o my-org DonationRollupTest # one class
renmy record -o my-org --namespaces Nebula,npe01
renmy record -o my-org --data # settings + metadata rows tooSignatures and stable values land in stubs/, per-call answers in .nimbus/recordings/. Stubs are plain Apex, cassettes plain JSON; commit both and the whole team runs against the same answers. From then on renmy test replays the cassettes with no org, credentials or network, so CI runs it too. The package stays a black box, but it now runs on your machine.
What the first pass writes
The first half is available on its own as renmy stub pull, with nothing executed in the org:
renmy 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 the exact global surface: every overload, constructor, property, inner class and enum. Builder methods hand back constructed objects, so chains like
Nebula.Logger.info('x').addTag('y')work. - Constant values. A SymbolTable carries no values for global constants, so one short anonymous Apex script per class reads them from the org.
- Labels, fields, objects. Custom labels, plus field and object schema for everything your project references. This is where
npe01__Private__ccomes from. - Data, opt-in with
--data. Custom-setting rows become seed lines inrenmy.properties; custom-metadata records becomestubs/customMetadata/files.
Re-running is safe: anything your project defines, and anything already under stubs/, is left alone. The flag reference is in the docs.
What the second pass captures
The pull covers everything with a stable answer. It cannot cover per-call behaviour: what a package method returns for specific arguments. That is the recording half, available on its own once stubs exist:
renmy test --record -o my-org MyTestClass
# Forwards managed-package static calls to the org and writes
# what they return to .nimbus/recordings/.
renmy test MyTestClass
# Replays from the cassettes. No org connection.
renmy 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 renmy record bundles the pull. A call that was never recorded falls back to its stub body rather than failing. The pattern is VCR (Ruby), WireMock (Java) or Polly (JavaScript), applied to a package you cannot read.
What recording does not do
- Recorded calls really execute in the org, with their normal side effects. Point
-oat a scratch or developer org, not at production. - Static methods only. A local stub instance has no counterpart in the org, so instance methods keep their stub bodies.
- Callouts are not recorded. The platform refuses a callout in a test without
Test.setMock, so a deployable test already ships its own mocks (how). - The package’s internals stay hidden. The cassette holds what the call returned, not what the package did on its own side of it. Your code is the thing under test.
Both renmy record and hand-written stubs are Free.
Generating stubs from your code with renmy stub auto (Pro)
renmy stub auto does the walk a hand-written stub needs: it scans the project AST, finds every reference to a class renmy runtime cannot resolve, infers the surface from how your code uses it, and writes one .cls per class.
renmy stub auto --dry-run # preview without writing
renmy stub auto # generate (skips files that already exist)
renmy stub auto --merge # appends only new methods; keeps your edits
renmy stub auto --force # full rewrite (discards hand edits)
# Or generate after a successful test run
renmy test --write-stubs --write-stubs-mergeIt captures method names and arities as your code calls them, argument types from declared locals, return types from context, generics and constructors. It does not capture parameter names (expect arg0, arg1) or bodies: generated stubs count calls so tests can assert on invocation, but return a type default. Hand-edit when you need more; --merge appends only what is new and preserves your edits. --update-config registers any namespace the walker detects in renmy.properties, so Pkg.Logger is not mistaken for an outer class.
renmy stub auto is Pro, and Pro is free during the public beta (pricing).
Does the runtime actually run package-grade Apex?
A stub only helps if the runtime underneath behaves like the platform, and that claim should come with a record. renmy runtime’s is public: the parity record runs 33,415 Apex tests from 76 open-source projects, cloned unmodified and pinned to a commit, with no org attached; 98.2% produce identical results locally, and every failure is re-run on a real Salesforce org and classified.
Three rows matter here. NPSP: 3,985 tests, 3,979 passing locally, 99.8%, all six failures confirmed on an org as renmy runtime bugs. EDA: 2,355 of 2,357. nebula-logger: 1,354 of 1,354, which reproduces in a few minutes:
git clone https://github.com/jongpie/NebulaLogger
cd NebulaLogger
git checkout 05ce16c5
renmy test "*"Be precise about what this proves. Those rows run the package’s source, the position an ISV is in: build the package and your own suite runs locally with nothing to stub. Subscribe to it and its code is still (hidden), so stubs or recordings are the bridge. The record is not a claim of 100% parity; it is the measurement of how close, with every failure itemised.
Which approach to use
- Your code only logs to the package. Run the tests; the auto-null fallback is enough. Add
renmy.stubs.namespacesto keep CI quiet. - Your tests assert on a few return values. Write the stub by hand under
stubs/<Pkg>/. Free. - Your code reads the package’s fields, labels or settings.
renmy record -o my-orgonce, commit, thenrenmy testoffline. Free. - The codebase keeps growing against the package.
renmy stub auto --merge. Pro, free during the beta. - You are the ISV. Clone,
renmy test "*", compare with the parity record. If the package queues or schedules work, see testing async Apex locally.
Your sandbox will run these tests too, one deploy and one queue at a time. Recording means the org answers once and the laptop answers every time after, in tens of milliseconds, including on a CI runner with no credentials.
Try it
curl -fsSL https://renmy.net/install.sh | sh # macOS, Linux
irm https://renmy.net/install.ps1 | iex # Windows PowerShell
cd your-sfdx-project
renmy test "*" # see which failures are missing dependencies
renmy record -o my-scratch-org # once; every run after is offlineThe quickstart takes you from install to a first green run; the managed packages page has the full stub and recording reference. Source, issues and released binaries are on GitHub.
Frequently asked questions
Can I run Apex tests that call a managed package without a Salesforce org?
Yes. renmy runtime runs Apex tests locally against an embedded PostgreSQL. A managed package’s hidden classes are covered either by a hand-written stub in stubs/ or by renmy record -o <org>, which asks a scratch or developer org once for the package’s surface, values and per-call answers and writes them to stubs/ and .nimbus/recordings/. After that, renmy test replays with no org, credentials or network.
Does renmy record need a production org?
No, and it should not use one. Recorded calls really execute in the org with their normal side effects, so point -o at a scratch or developer org that has the package installed. Recording is a one-time step; commit what it writes and the team runs without the org.
What does renmy record not capture?
Instance methods are not forwarded, because a local stub instance has no counterpart in the org, so they keep their stub bodies. Callouts are not recorded, because the platform refuses a callout in a test without Test.setMock. A package’s non-global internals stay out of reach; the cassette holds what a call returned, not what the package did internally. And only calls into classes under stubs/ are intercepted.
Does the parity record mean NPSP runs locally?
The parity record runs NPSP’s open-source repository from source, unmodified, and 3,979 of its 3,985 tests pass locally. That proves the runtime executes the Apex NPSP is made of. In a subscriber org the package’s class bodies are still hidden, so a nonprofit developer testing their own code against NPSP records or stubs the package surface they call.
Record the package once, then test without the org
renmy runtime runs your Apex tests against your working tree on an embedded PostgreSQL. Hand-written stubs and renmy record are Free; one scratch org, once, and every run after is offline.