# Testing

> Discover, select, run and measure JavaScript, TypeScript and JVM 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, and stops at any nested project (a directory with
`elide.pkl`, `package.json`, `pom.xml` or `requirements.txt`). A workspace root therefore finds no member tests;
naming a path still runs it.

```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 found by convention; a declared 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 while `jvm.features.testing` is `true` (the default).
- `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 discovery:

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

Path arguments skip JVM tests, reporting `Path filtering unsupported for JVM tests: <paths>`. `-t` matches test names
in both languages:

```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 narrow what the selectors chose.

## Options

| Option | Effect |
| --- | --- |
| `-t REGEX` | Match test or enclosing-suite names. |
| `--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)).

## 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 the manifest enables them
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>` marks the start of a test; its output lines are `# out <id>: <text>`. `# out: <text>` is not
  attributed to a test.
- Test points are numbered `1..N` in the order tests finish. Join 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, when it has one.
- Stderr is not framed: `elide test --reporter=tap 2>&1` is not valid TAP.
- JVM: launcher output is attributed 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, and at a file's top level after importing `node:test`, is framed. Output
  before that import is raw. Hook output is attributed 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. The console prints one combined summary.

The top-level `coverage` block enables collection 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.

## Limits

- No Python test discovery.
- 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).
