# Testing

> Run JavaScript and TypeScript `node:test` files and JUnit tests with `elide test`: discovery, selecting tests, options, reports and coverage.

`elide test` runs JavaScript and TypeScript `node:test` files and JUnit tests.

```bash
elide test
```

A failing test makes the exit code non-zero.

<img src="https://elide.help/assets/demos/test.gif" alt="Discovering and running JavaScript tests with Elide" width="1591" height="764" loading="lazy" decoding="async" class="rounded-lg border border-border my-6 w-full" />

## JavaScript and TypeScript

Tests use the `node:test` API and need no manifest. A file is a test if it matches `*.test.*`, `*.spec.*`,
`*_test.*` or `*_spec.*`, or is under `test`, `tests` or `__tests__`. Extensions: `.js`, `.mjs`, `.cjs`, `.ts`,
`.mts`, `.cts`.

Discovery skips hidden, dependency and build-output directories. It stops at any nested project (a directory with
`elide.pkl`, `package.json`, `pom.xml` or `requirements.txt`), so a workspace root finds no member tests. Name a path
to run them.

```js
import test from "node:test";
import assert from "node:assert";

test("adds two values", () => {
  assert.equal(1 + 1, 2);
});
```

## JVM tests

JVM tests run on the JUnit Platform and need an `elide.pkl`. `src/test` is the default test source set; a declared
source set of type `test` replaces it ([Source Sets](https://elide.help/docs/toolchain/source-sets)).

- Test sets get `testPackages` plus the main set's dependencies.
- Bundled JUnit libraries are added by default. `jvm.features.testing = false` removes both the JUnit compile APIs and
  the bundled runtime; a JVM test run then needs a JUnit console launcher (`junit-platform-console`) in `testPackages`,
  and fails with an error otherwise. Runs that include no JVM tests are unaffected.
- `processors` run on test sources.
- The test JVM gets `jvm.flags` and `jvm.defs` (as `-D`).
- `modules` dependencies of the test and main sets go on `--module-path` with `--add-modules=ALL-MODULE-PATH`.
- Parallel settings in a `junit-platform.properties` on the test classpath override `--concurrency`.

## Select tests

Paths, directories or globs narrow JavaScript and TypeScript tests:

```bash
elide test src/api
elide test src/api/client.test.ts
```

Path arguments skip JVM tests and print `Path filtering unsupported for JVM tests: <paths>`. `-t` matches test names
in both runners:

```bash
elide test -t 'parses .+ headers'
```

JVM tests take JUnit selectors and tag expressions:

```bash
elide test --select=class:com.example.ParserTest
elide test --select=method:com.example.ParserTest#parsesHeaders
elide test --select=package:com.example.parser   # the package and its subpackages
elide test com.example.parser                     # a dotted bare token is a package selector
elide test --include-tag=fast --include-tag=unit  # fast OR unit
elide test --exclude-tag=slow --exclude-tag=flaky # neither slow nor flaky
elide build test --select=class:com.example.ParserTest
```

<img src="https://elide.help/assets/demos/jvm-test.gif" alt="elide test — JUnit selectors on a Kotlin project" width="1591" height="873" loading="lazy" decoding="async" class="rounded-lg border border-border my-6 w-full" />

`--include-tag='slow & !flaky'` is one expression. Tag filters apply after selectors.

## Output and reports

Progress goes to stderr; the final tally goes to stdout:

```bash
elide test > summary.txt
elide test 2> progress.txt
```

Reporters write to `.dev/reports/tests/<language>/<source-set>/`. `testing.reports` in `elide.pkl` turns them on
without a flag:

```pkl
import "elide:Testing.pkl" as Testing

testing {
  reports { Testing.xmlReport() }
}
```

### TAP output

`--reporter=tap` makes stdout a pure TAP 13 stream:

```text
TAP version 13
# start 1: suite A > t1
# out 1: printed from inside a test
ok 1 - suite A > t1
# start 2: broken
not ok 2 - broken
  ---
  message: "expected 1 to equal 2"
  severity: "ERR_ASSERTION"
  ...
1..2
```

- `# start <id>` opens a test; its output lines are `# out <id>: <text>`. `# out: <text>` belongs to no test.
- Test points are numbered `1..N` in finishing order. Match points to tests by the `# start` line, not the number.
- A YAML block appears only on failures with details; `severity` is the runner's error code, if any.
- Stderr is not framed: `elide test --reporter=tap 2>&1` is not valid TAP.
- JVM: output is tagged with its test only while exactly one test runs. With `--concurrency` above 1 or a project
  `junit-platform.properties`, it is untagged.
- JavaScript: `console.log` inside a test, or at top level after importing `node:test`, is framed. Output before that
  import is raw. Hook output belongs to the test the hook wraps; `t.diagnostic` is framed as `# out`.

## Coverage

```bash
elide test --coverage
elide run --coverage
```

JVM coverage goes to `.dev/reports/coverage/jvm/<task>/` as HTML, XML or CSV; JavaScript and TypeScript coverage is
LCOV, in `.dev/reports/coverage/js/lcov.info` for tests and `.dev/reports/coverage/js/run/lcov.info` for runs. The
console prints one combined summary. The top-level `coverage` block collects without the flag:

```pkl
import "elide:Coverage.pkl" as Coverage

coverage {
  enabled = true
  scope = "tests"
  jvm {
    excludes { "org.example.generated.*" }
    reports {
      Coverage.xmlReport()
      Coverage.htmlReport()
    }
  }
  js {
    paths { "src" }
  }
}
```

| Property | Meaning | Default |
| --- | --- | --- |
| `enabled` | Collect without `--coverage`. | `false` |
| `scope` | `"tests"`, `"runs"` or `"all"`; `--coverage` always covers its own task. | `"tests"` |
| `jvm.reports` | `Coverage.htmlReport()`, `xmlReport()`, `csvReport()`. | none |
| `jvm.excludes` | Class patterns (`*`, `?`) not instrumented, such as generated code. | none |
| `js.paths` | Source roots to report; empty means the whole project. | none |

With coverage, `elide run` starts a separate JVM process.

## Options

| Option | Effect |
| --- | --- |
| `-t REGEX` | Match test or enclosing-suite names. |
| `--where CEL` | JavaScript: run tests for which a [CEL](https://elide.help/docs/api-cel) expression over `name`, `parents` and `file` is true. |
| `--select=SELECTOR` | JVM: `class:`, `method:` or `package:` selector; repeatable. |
| `--include-tag=EXPR` | JVM: run tests whose tags match; repeats are ORed. |
| `--exclude-tag=EXPR` | JVM: skip tests whose tags match; repeats are ANDed. |
| `--bail[=N]` | Stop after `N` failures (default 1). |
| `--test-timeout=MS` | Default per-test timeout. |
| `--timeout DURATION` | Time limit for the whole invocation (global). |
| `--only` | Run only tests marked `.only`. |
| `--concurrency=N` | Run files or JVM classes in parallel. Default `1`; `0` is invalid. |
| `--coverage` | Collect coverage; see [Coverage](#coverage). |
| `--reporter=junit` | Write JUnit XML reports. |
| `--reporter=tap` | Write TAP 13 to stdout (and under `--reporter-outfile`). |
| `--reporter-outfile=DIR` | Report directory. |
| `--debugger[=MODE\|ADDRESS]` | Attach a debugger; see below. |
| `--debugger-host=HOST`, `--debugger-port=PORT` | JavaScript debugger address (DAP: 4711; CDP: 1337). |

| `--debugger` form | Debugs |
| --- | --- |
| bare | JVM tests over JDWP on `127.0.0.1:5005`, and JavaScript tests. |
| `=5005`, `=host:5005` | JVM tests only (JDWP). |
| `=dap`, `=cdp` | JavaScript tests only. |

Grammar: [Debugging](https://elide.help/docs/debugging). An unknown option or missing value fails before anything runs
([Build Tasks](https://elide.help/docs/toolchain/build-tasks#options)).

## Limits

- No Python test discovery; run unittest or pytest instead ([Python tests](https://elide.help/docs/python-testing)).
- JVM tests need a manifest.
- Not implemented: `node:test` snapshots, `mock.module`, watch mode, programmatic `run()`, `node:test/reporters`.

Supported API: [Node test API](https://elide.help/docs/api-node-test).

## Samples

- [TypeScript tests](https://elide.help/docs/code-samples#typescript-tests)
- [Kotlin tests](https://elide.help/docs/code-samples#kotlin-tests)
- [Filter tests with CEL](https://elide.help/docs/code-samples#filter-tests-with-cel)
- [JUnit XML in CI](https://elide.help/docs/code-samples#junit-xml-in-ci)

Start from a template:

```bash
elide init --template java        # Java app with a JUnit test
elide init --template ktjvm       # Kotlin app with a test
```

## Shared test containers

Declare disposable services in `testing.testcontainers`. Elide starts them before the project's selected
JavaScript, TypeScript, and JUnit runners, waits for readiness, and stops them when the test run finishes.
Every runner in that project receives the same connection values. This works independently of Micronaut
or any other application framework.

```pkl
amends "elide:project.pkl"
name = "database-tests"

testing {
  testcontainers {
    containers {
      ["postgres"] {
        image = "postgres:17-alpine"
        environment {
          ["POSTGRES_USER"] = "test"
          ["POSTGRES_PASSWORD"] = "test"
          ["POSTGRES_DB"] = "app"
        }
        ports { 5432 }
        wait { timeoutSeconds = 90 }
      }
    }
    environment {
      ["DATABASE_HOST"] = "${postgres.host}"
      ["DATABASE_PORT"] = "${postgres.port.5432}"
      ["JDBC_URL"] = "jdbc:postgresql://${postgres.host}:${postgres.port.5432}/app"
    }
  }
}
```

Read these values through `process.env` in JavaScript/TypeScript or `System.getenv` in Java/Kotlin.
Bindings override inherited environment variables for the test runners only. A binding can include
`${name.host}`, `${name.port.CONTAINER_PORT}`, or `${name.id}`; unknown references fail before startup.
Ports are TCP ports inside the container. Testcontainers assigns available host ports, so concurrent runs
do not need fixed port reservations. Workspace members have independent container sets.

Each container supports `image`, an optional `command` argument list, `environment`, `ports`, and `wait`:

| Wait strategy | Required settings | Behavior |
| --- | --- | --- |
| `"port"` (default) | Nonempty `ports` | Wait for exposed ports to listen |
| `"http"` | `wait.port` naming an exposed port | GET `wait.path` (default `/`), expecting `wait.status` (default `200`) |
| `"log"` | `wait.pattern` | Wait for one matching Java regular expression in container logs |

`wait.timeoutSeconds` defaults to 60. The overall `startupTimeoutSeconds` defaults to 300 and includes
image downloads and all readiness checks. Startup failures fail the test run. Container command arguments
are passed directly; use an explicit shell command inside the container if needed.

A Docker-compatible daemon and JDK 17 or newer with source-file launching support are required. Elide uses its
configured JVM toolchain and resolves Testcontainers separately from application dependencies on first
use. Docker connection settings such as `DOCKER_HOST` are inherited. This path is also used by the native
Elide CLI; Testcontainers runs in a helper JVM. It adds nothing to your application JAR or Native Image.

`elide install` and compilation do not start containers. `elide test` retains normal test selection,
reporting, and coverage behavior; `elide build test` supports containers for JVM test tasks too. Cleanup
runs after success, failure, or cancellation. The helper monitors its parent connection, and Testcontainers'
resource reaper handles orphaned Docker resources. Keep the resource reaper enabled in CI.

Session files live under `.dev/testcontainers/`. Requests containing container environment values are
deleted after the helper reads them and again during cleanup. Helper logs remain there for diagnosis;
they can contain output from the container library. On POSIX systems, session directories are private.

The `testcontainers` project sample contains JavaScript and JUnit tests against one shared HTTP container.
Run it with `elide test`. For this repository's Docker-backed integration checks, run
`make test-testcontainers JVM=yes` after a JVM build, or `make test-testcontainers` against a native build.
