utPLSQL for VS Code
Run utPLSQL unit tests directly from Visual Studio Code's
native Testing API — Test Explorer, gutter run icons, inline failure locations, code coverage
and reporter export, without needing SQL Developer, PL/SQL Developer or the utPLSQL-cli.
[!IMPORTANT]
This is not an official utPLSQL project. It is an independent, community-built extension
that talks to utPLSQL's public database API. It is not affiliated with, endorsed by, or
maintained by the utPLSQL project or its contributors.
utPLSQL itself is a separate project under its own licence.
Please report anything wrong with this extension in
its own issue tracker — not to the
utPLSQL project, which cannot support it.
What you get

Your suites appear in VS Code's own Testing view. They are discovered from the database, not
from your files, so the tree is whatever ut_runner.get_suites_info actually reports —
--%suite, --%context, --%suitepath groups, tags and --%disabled markers included.
- Live progress. Results stream in one test at a time via the real-time reporter protocol,
so a long suite tells you about failure #1 immediately instead of after the last test.
- Failure locations that land where the bug is. A failed expectation jumps to the failing
line inside the package body, not to the test procedure's header.
- Lazy tree. Expanding a node resolves only that level. A schema with hundreds of packages
does not have to be turned into
TestItems before you can click anything.
How it works

The extension connects straight to the database through
node-oracledb in Thin mode. There is no Oracle Client
to install, no CLI to keep on your PATH, and no intermediate process — which is also why the
only credentials involved are the ones you give it, stored in VS Code's SecretStorage.
Everything it shows you comes from three utPLSQL entry points: ut_runner.get_suites_info for
the tree, ut_realtime_reporter for live results, and the coverage reporters for per-line hits.
Requirements
- Oracle Database with utPLSQL >= 3.1.3 installed (>= 3.1.4 for the real-time reporter used
for running tests; discovery and reporter export need >= 3.1.3 for
get_suites_info).
- VS Code 1.137 or newer.
- Network access from the machine running VS Code to the database.
Getting started

1. Add a connection
Run utPLSQL: Add Connection from the Command Palette (Ctrl+Shift+P). You will be asked for:
| Prompt |
What to enter |
| Name |
Anything you like — it becomes the root node's label, e.g. dev. |
| User |
The database user that owns or can see the test packages. |
| Connect string |
Easy Connect (host:1521/SERVICE) or a TNS alias. Prefix with tcps:// for a TLS-encrypted connection. Pick Enter manually if you have no tnsnames.ora. |
| Default schema |
Optional. Leave empty to use the connecting user's own schema. |
| Wallet directory |
Optional, for a tcps:// connection. Must contain ewallet.pem — the only wallet file the Thin-mode driver reads. An Autonomous Database wallet download already includes it; a classic mutual-TLS wallet made via orapki/mkstore may need orapki wallet export ... -pem to produce one. |
| Wallet password |
Only asked if a wallet directory was given. Optional (an auto-login wallet needs none); stored in SecretStorage. |
| Password |
Stored in VS Code's SecretStorage — never in settings.json, never logged. |
An empty password is accepted and recorded as "no password yet"; run utPLSQL: Set Password for
Connection before using the profile. Likewise, utPLSQL: Set Wallet Password for Connection
updates or clears a wallet's password later without re-adding the profile.
2. Expand it in the Testing view
Open the Testing view (the flask icon in the Activity Bar). Your profile is the root node,
labelled with the utPLSQL version detected on that connection. Expanding it discovers schemas
that actually contain suites, then the suite tree per schema.
If nothing appears, the connection reached the database but found no suites — see
Troubleshooting.
3. Run
Three ways, all equivalent:
- the run icons in the Testing view (a single test, a package, a whole schema, or a multi-select),
- the gutter icons next to
--%suite / --%test annotations in your source files,
- utPLSQL: Run Test at Cursor.
4. Add coverage
Use the Run with Coverage profile instead of plain Run.

The covered scope is derived, not guessed: what the test packages depend on, minus the test
packages themselves. Objects reached only dynamically (execute immediate, triggers) never
appear in *_dependencies and so are never found automatically — add them with
utplsql.coverage.includeObjects.
Working without local source files
None of the above needs a local copy of your PL/SQL. If the database is your only source of
truth, the extension stays fully usable:
- The tree is built from the database either way.
- Failure locations and coverage open a read-only
utplsql-source:// document with the source
fetched from the database.
- Run Test at Cursor, Run with Reporter (Export) and Generate Test Package fall back
to a QuickPick of discovered objects when there is no cursor to resolve.
- Right-clicking an item in the Testing view offers Run with Reporter (Export) and
Generate Test Package for that item directly.
If you do keep sources in the workspace, they need a language ID the extension recognises
(sql or oracle-sql by default, see utplsql.discovery.languageIds) so objects can be mapped
to files. An oracle-sql language is contributed for the common extensions (.pkb, .pks,
.pls, .plb, .tps, .tpb, .prc, .fnc, .trg, .vw); add files.associations entries
yourself for anything else.
Exporting results
utPLSQL: Run with Reporter (Export) runs any output reporter your utPLSQL install offers —
documentation, JUnit, TAP, TeamCity, Sonar and whatever else ut_runner reports — and writes it
to the Output channel or a file.
The Export with Reporter run profile does the same for an arbitrary Test Explorer selection,
producing one file per connection profile involved. Both are cancellable; cancelling drops the
partial output rather than saving a truncated report as if it were finished.
Troubleshooting
| Symptom |
Cause and fix |
| The profile appears but expanding it shows nothing |
The user can connect but sees no suites. Check you are looking at the right schema (defaultSchema), and that the packages really carry --%suite. utPLSQL needs a blank line before the first --%test or the package is silently invisible to get_suites_info. |
A newly added --%test does not show up |
utPLSQL caches annotations. Run utPLSQL: Rebuild Annotation Cache, which rebuilds it and refreshes the tree. |
ORA-01017 when running |
The stored password no longer matches. Run utPLSQL: Set Password for Connection. The pool is rebuilt on the next attempt, so the fix takes effect immediately. |
| "no free connection … pool max" |
A run, coverage build, tree resolve or export is using every pooled connection. Wait for it to finish. |
| Coverage shows 0% for an object you know is exercised |
It is probably outside the derived scope — reached only via execute immediate or a trigger. Add it to utplsql.coverage.includeObjects. |
| Failure locations open an empty editor |
No local file matched the object, and the virtual document could not be fetched. Check the user can read the source (all_source/dba_source). |
| Everything is slow and you want to know why |
Set utplsql.perf.enabled and read the utPLSQL output channel; see docs/performance.md. Use utplsql.trace for per-event detail — it is noticeably slower, so leave it off otherwise. |
Commands
| Command |
Description |
utplsql.addConnection |
Add a new connection profile. |
utplsql.setPassword |
(Re-)store a connection's password in SecretStorage. |
utplsql.removeConnection |
Remove a connection profile and its stored password. |
utplsql.runTestAtCursor |
Run the suite/test/package at the cursor position. Without a usable cursor (a virtual utplsql-source:// document, or no local source file at all) falls back to a QuickPick of the profile's discovered database objects. |
utplsql.runWithTags |
Run all tests carrying one or more chosen --%tags(...) values, for a connection profile. |
utplsql.rebuildAnnotations |
Rebuild utPLSQL's own --%annotation cache for a profile's discovered schemas, then refresh the Test Explorer — use this if a newly added/changed --%test isn't showing up after a recompile. |
utplsql.runWithReporter |
Run the package at the cursor, the right-clicked Test Explorer item, or one picked from a QuickPick, with a chosen output reporter, to the Output channel or a file. The Export with Reporter run profile does the same for an arbitrary Test Explorer selection, one file per connection profile involved. |
utplsql.generateTest |
Generate a test package skeleton for the unit at the cursor, the right-clicked Test Explorer item, or one picked from a QuickPick of the profile's testables(). |
Settings
| Setting |
Default |
Description |
utplsql.connections |
[] |
Connection profiles (name, user, connectString, optional defaultSchema, optional walletLocation). Managed via the commands above; edit directly only if you know what you're doing. |
utplsql.connections.tnsAdminPath |
"" |
Folder containing tnsnames.ora. Falls back to the Oracle SQL Developer extension's sqldeveloper.connections.tnsConfiguration.path — but only a user/machine-level value for it, never one set by a workspace, since that setting's scope belongs to that extension and is not ours to restrict — then to TNS_ADMIN. If empty and you enter a TNS alias directly as the connect string, node-oracledb still resolves it itself at connect time. |
utplsql.discovery.languageIds |
["sql", "oracle-sql"] |
Language IDs treated as PL/SQL source for discovery and parsing. |
utplsql.run.randomOrder |
false |
Run tests in a random order (a_random_test_order) instead of declaration order, to surface hidden order dependencies between tests. |
utplsql.run.randomOrderSeed |
0 |
Seed for utplsql.run.randomOrder. 0 leaves the seed unset (a new one every run, not reproducible); a positive value reproduces the same order every run. |
utplsql.coverage.htmlReport |
false |
Also run ut_coverage_html_reporter, writing the report to a temporary file and offering it via a notification (Open in Browser / Save As…) instead of rendering it in a webview — its HTML is assembled by the database from database-derived text (schema/object names, source lines) that on a shared schema you may not have written yourself. |
utplsql.coverage.excludeObjects |
[] |
Additional object names (e.g. UT, UT_EXPECTATION) excluded from the automatically derived coverage scope — e.g. when utPLSQL itself is installed in the same schema as the code under test. |
utplsql.coverage.schemes / utplsql.coverage.includeObjects |
[] |
Fully override the automatically derived a_coverage_schemes/a_include_objects — useful for objects reached only dynamically (execute immediate, triggers), which never show up in *_dependencies. Empty = automatic. |
utplsql.coverage.includeSchemaExpr / includeObjectExpr / excludeSchemaExpr / excludeObjectExpr |
"" |
Regex-based coverage scoping (a_include_schema_expr etc.), applied on top of the automatic/overridden scope. |
utplsql.coverage.reporter |
"sonar" |
The native Coverage view always uses ut_coverage_sonar_reporter; set to "cobertura" to additionally run ut_coverage_cobertura_reporter and get offered a save dialog for it after each coverage run. |
utplsql.reporter.clientCharacterSet / utplsql.reporter.colorConsole |
"" / false |
a_client_character_set/a_color_console for utplsql.runWithReporter and the Export with Reporter run profile. |
utplsql.generate.* |
see package.json |
Test generation options: package/unit prefix/suffix, tests-per-unit, comments, disabled-by-default, suite path, indent. Mirrors utPLSQL's SQL Developer test generator settings. |
utplsql.trace |
false |
Detailed per-event logging (suite/test start and end, one line per received real-time reporter event) to the utPLSQL output channel. Useful for troubleshooting; noticeably slower on large test runs (see utplsql.perf.enabled), so leave it off otherwise. |
utplsql.perf.enabled |
false |
Timing instrumentation for discovery/run steps (tree build, event throughput) to the utPLSQL output channel. See docs/performance.md. |
utplsql.perf.reportFile |
"" |
When set and utplsql.perf.enabled is on, also appends each timing measurement as a JSON line to this file. |
Known limitations
- No PL/SQL debugger. VS Code has no built-in PL/SQL debug adapter for this extension to
drive, so stepping through PL/SQL is out of scope for now.
- Suite path collisions across schemas. utPLSQL suite paths don't carry the owning schema.
Running suites that share the same path in two different schemas in a single run can
misattribute a live event to the wrong schema's test item — the same known limitation Oracle
SQL Developer's utPLSQL integration has.
DATE columns in a sys_refcursor comparison. Without ut.set_nls/ut.reset_nls around
the comparison, DATE values are compared using the session's default NLS date format instead
of as a proper date-time value. ut.set_nls must stay active through both OPENs and the
to_equal/to_contain call itself — calling ut.reset_nls any earlier raises ORA-01861
(confirmed against a live utPLSQL 3.2.3 instance; ut.pks's own doc comment only says
ut.reset_nls must run "after refcursor is open", which is necessary but not sufficient). See
the ut-nls-cursor snippet for the verified sequence.
Development
npm install
npm run test # typecheck + unit tests (no database needed)
npm run watch # esbuild --watch, for iterating in the Extension Development Host (F5)
npm run package # build a .vsix with vsce
Integration tests need a real utPLSQL-equipped Oracle instance. A ready-made one is available via
Docker. The credentials it uses (ut3 / oracle, overridable via ORACLE_PASSWORD) are
throwaway defaults for a local, disposable container — never point this compose file at anything
reachable from outside your machine:
docker compose up -d --build # starts gvenzl/oracle-free with utPLSQL installed into schema UT3
npm run test:integration
The container is ready in well under a minute: the faststart base image ships an already-created
database, and only the utPLSQL install runs at first startup.
docker-compose.override.yml adds a named volume so that database survives a container rebuild.
Compose picks that file up automatically — but note the volume is populated once and never
refreshed afterwards, so changes to docker/oracle-utplsql/init-scripts/ (a new utPLSQL version,
say) only take effect after docker compose down -v. CI sets COMPOSE_FILE explicitly and
therefore runs without the volume, which is deliberate: an empty volume mounted over
/opt/oracle/oradata hides the prebuilt database and forces a full 45-minute recreation.
Point the integration tests at a different instance/schema with the UTPLSQL_IT_USER,
UTPLSQL_IT_PASSWORD and UTPLSQL_IT_CONNECT_STRING environment variables (defaults match the
docker-compose.yml setup above).
The fixture also provisions a second, deliberately unprivileged user (utplsql_vsc_unpriv,
holding nothing but CREATE SESSION) and grants the utPLSQL schema read access to
v$session plus dba_objects/dba_dependencies/dba_source. Both are there for tests that
cannot be written without them: the session-lifecycle assertions in cancel.test.ts and
pool.test.ts need to observe sessions from outside themselves, and the per-profile
dba_/all_ caching regression needs two connections that see the data dictionary
differently. Override the second user with UTPLSQL_IT_UNPRIV_USER and
UTPLSQL_IT_UNPRIV_PASSWORD; where it does not exist, the tests that need it skip themselves
rather than fail.
The diagrams in this README are authored as SVG in docs/images/ and rasterised to PNG alongside
them — the VS Code Marketplace rejects SVG in a README, so the PNGs are what the README
references. The extension icon is the same arrangement: docs/images/icon-source.png is the
full-resolution original, and the shipped icon.png is a 256×256 rendering of it, which is what
keeps the packaged .vsix at a couple of hundred KB rather than a megabyte. Re-render either
after editing (any headless Chromium will do):
chrome --headless --force-device-scale-factor=2 --window-size=900,470 \
--screenshot=docs/images/test-explorer.png docs/images/test-explorer.svg
Credits
This extension's database protocol layer is a TypeScript port of
utPLSQL for SQL Developer (Apache-2.0) — its
UtplsqlDao/RealtimeReporterDao, event model, PL/SQL parser and test generator are
IDE-independent and were reused rather than reinvented. See NOTICE for the full
attribution.
That reuse is what the Apache-2.0 licence permits, and nothing more: it does not make this
extension part of the utPLSQL project, and the utPLSQL name is used here only to describe what
the extension integrates with. All thanks for the framework itself go to the utPLSQL
maintainers — the bugs in this extension are mine.
Security
- Passwords are stored only in VS Code's
SecretStorage, never in settings and never written to
the output channel.
utplsql.connections, utplsql.connections.tnsAdminPath, utplsql.perf.enabled and
utplsql.perf.reportFile are all machine-scoped: a workspace cannot contribute or override
any of them, so opening someone else's repository can't redirect a connection profile (and its
stored password) at another host, or turn on performance logging to a workspace-chosen file.
utplsql.connections.tnsAdminPath's fallback to the Oracle SQL Developer extension's
sqldeveloper.connections.tnsConfiguration.path only honours a value set at user/global level,
never one from a workspace, since that setting's scope belongs to that extension and is not
ours to restrict. Without that check, a workspace could still redefine the TNS alias a
stored-password connection profile names and redirect that connection to another host, despite
our own settings being machine-scoped.
utplsql.perf.reportFile is additionally validated at write time: even a value configured at
user/machine level is only used if it resolves inside an open workspace folder, so a mistyped or
unexpected path can't append to an unrelated file elsewhere on disk.
- The coverage HTML report is opened in your browser rather than rendered in a webview. Its
markup is assembled by the database from object names and source lines; utPLSQL 3.2.3 escapes
them, but the extension does not rely on that staying true at every version.
- The extension is disabled in untrusted workspaces.
Found a security issue? Please report it via
GitHub Security Advisories
rather than a public issue.
License
Apache-2.0 — see also NOTICE.