# Test packages

This guide shows you how to add tests to your package. You’ll learn how to write test files, use inline inputs, and run the test harness.

Make testing part of your workflow

Tests are your safety net during development. The baseline-driven workflow lets you iterate quickly: change code, run tests, review diffs, update baselines. Start with a few key tests and expand coverage as your package grows.

## Test file structure

Place test files in the `tests` directory of your package:

* acme/

  * tests/

    * normalize.input Sample data for the test

    * normalize.tql Test file

    * normalize.txt Expected output baseline

    * context/

      * test.yaml Suite configuration
      * 01-update.tql First test in suite
      * 01-update.txt
      * 02-inspect.tql Second test in suite
      * 02-inspect.txt

Each test consists of:

* A `.tql` file containing the test pipeline
* An optional `.input` file with test-specific data
* A `.txt` file with the expected output baseline

## Use inline inputs

**Inline inputs** are the preferred way to provide test data. Place a `.input` file next to your test file with the same base name:

* tests/

  * parse-csv.input Input data
  * parse-csv.tql Test file
  * parse-csv.txt Expected baseline

The harness exposes the input file path via `TENZIR_INPUT`:

tests/parse-csv.tql

```tql
from_file env("TENZIR_INPUT") {
  read_csv
}
```

tests/parse-csv.input

```csv
name,value
Alice,42
Bob,17
```

Inline inputs keep test data next to the test that uses it, making tests self-contained and easy to understand.

### When to use shared inputs

For data shared across multiple tests in a subdirectory, create a local `inputs/` directory. The harness uses the **nearest `inputs/` directory** when resolving `TENZIR_INPUTS`:

* tests/

  * network/

    * inputs/ Shared by tests in network/ and children

      * packets.pcap
      * flows.json

    * tcp/

      * analysis.tql TENZIR\_INPUTS → ../inputs/

    * udp/

      * stats.tql TENZIR\_INPUTS → ../inputs/

  * inputs/ Fallback for tests without a closer inputs/

    * common.json

Access shared inputs in TQL:

tests/network/tcp/analysis.tql

```tql
from_file f"{env("TENZIR_INPUTS")}/packets.pcap"
acme::analyze
```

Place `inputs/` directories as close to the tests that use them as possible. This keeps related data together and makes it clear which tests depend on which files. Prefer inline `.input` files for single-test data and local `inputs/` directories for data shared within a test group.

## Write test pipelines

Test pipelines exercise your package logic with known input and produce deterministic output. The most common pattern is testing user-defined operators (UDOs), which are the primary way to build reusable building blocks. However, you can test any TQL code, including standalone pipelines or complex workflows.

### Test an operator

tests/normalize.tql

```tql
from_file env("TENZIR_INPUT")
acme::normalize
```

tests/normalize.input

```json
{"@timestamp": "2024-01-15T10:30:00Z", "msg": "test"}
```

### Test OCSF mappings

OCSF mapping tests should exercise the package mapper and run the shared OCSF helpers before comparing against a baseline. This verifies both your mapping logic and the final schema shape that downstream pipelines consume.

tests/ocsf/auth.tql

```tql
from_file env("TENZIR_INPUT") {
  read_json
}
acme::ocsf::map
ocsf::derive
ocsf::cast
```

Use [`ocsf::derive`](https://preview.docs.tenzir.com/375/375/reference/operators/ocsf/derive.md) to populate sibling enum fields and [`ocsf::cast`](https://preview.docs.tenzir.com/375/375/reference/operators/ocsf/cast.md) to validate the mapped event against the OCSF schema. This lets the mapper stay minimal while the baseline captures the comprehensive OCSF shape that consumers see. Drop or replace non-deterministic fields before the comparison, such as processing timestamps created with [`now`](https://preview.docs.tenzir.com/375/375/reference/functions/now.md).

### Test with different arguments

Create separate test files for different argument combinations:

tests/tag-defaults.tql

```tql
from {hash: "abc123"}
acme::tag indicator
```

With a custom prefix:

tests/tag-with-prefix.tql

```tql
from {hash: "abc123"}
acme::tag indicator, prefix="IOC: "
```

### Test error conditions

Use the `error` frontmatter to expect non-zero exit codes:

tests/invalid-input.tql

```tql
---
error: true
---


from {invalid: null}
acme::strict_parse
```

## Run tests

Run `tenzir-test` from the package root (where `package.yaml` lives) or from the `tests/` subdirectory. The harness auto-detects package mode and configures paths accordingly.

### Preview output in passthrough mode

First, run tests in passthrough mode to see the actual output:

```sh
uvx tenzir-test --passthrough
```

This streams output directly to the terminal without comparing against baselines.

### Update baselines

When the output looks correct, save it as the baseline:

```sh
uvx tenzir-test --update
```

This creates or updates `.txt` files next to each test. For example, `tests/normalize.tql` produces `tests/normalize.txt`.

### Compare against baselines

Run all tests and compare against saved baselines:

```sh
uvx tenzir-test
```

The harness reports differences between actual output and baselines. Use `--verbose` for detailed output during debugging.

### Run specific tests

Target individual tests or directories:

```sh
uvx tenzir-test tests/normalize.tql
uvx tenzir-test tests/context/
```

Use `-m`/`--match` to select tests by substring or glob pattern. Bare strings perform a substring match against the test’s relative path, so you no longer need to wrap keywords in wildcards:

```sh
uvx tenzir-test -m context
uvx tenzir-test -m create -m update
```

Patterns containing `*`, `?`, or `[` still use fnmatch glob syntax:

```sh
uvx tenzir-test -m 'tests/*/create.tql'
```

You can combine paths and patterns — the harness intersects both selections, running only tests that match both the path and a pattern:

```sh
uvx tenzir-test tests/context/ -m create
```

## Test frontmatter options

Control test behavior with YAML frontmatter:

tests/slow-test.tql

```tql
---
timeout: 60
---


// Long-running test pipeline
```

| Option     | Type    | Default   | Description                               |
| ---------- | ------- | --------- | ----------------------------------------- |
| `timeout`  | integer | 30        | Command timeout in seconds                |
| `error`    | boolean | false     | Expect non-zero exit code                 |
| `skip`     | string  | unset     | Skip test with reason                     |
| `fixtures` | list    | `[]`      | Fixtures to request                       |
| `runner`   | string  | by suffix | Runner name (`tenzir`, `python`, `shell`) |

## Troubleshooting

### Test fails with “file not found”

Verify the `.input` file exists next to the test file with the same base name. Check that you’re using `env("TENZIR_INPUT")` (singular) for inline inputs.

### Context not found

Ensure the test suite has `fixtures: [node]` in `test.yaml`. The node fixture automatically installs the package, creating defined contexts.

### Non-deterministic output

Tests must produce deterministic output. Use `sort` to order results, and avoid timestamps or random values in output. For time-based tests, use fixed input data rather than `now()`.

### Baseline mismatch after changes

Run `uvx tenzir-test --update` to regenerate baselines after intentional changes. Review the diff to verify the changes are expected.

## See also

* [Create a package](https://preview.docs.tenzir.com/375/375/guides/packages/create-a-package.md)
* [Add operators](https://preview.docs.tenzir.com/375/375/guides/packages/add-operators.md)
* [Add contexts](https://preview.docs.tenzir.com/375/375/guides/packages/add-contexts.md)
* [Test Framework](https://preview.docs.tenzir.com/375/375/reference/test-framework.md)
* [Write a package](https://preview.docs.tenzir.com/375/375/tutorials/write-a-package.md)