Nextflow nf-test snapshots to Galaxy/Planemo assertions
Translation guide for the nf-test → Galaxy direction. Companion to tests-format (assertion vocabulary), planemo-asserts-idioms (output-type → assertion-family decision table), and summary-nextflow (source-side schema). This note maps the snapshot shape specifically; per-output assertion picking (size delta, image dimensions, etc.) defers to planemo-asserts-idioms.
The corpus split is load-bearing: in the pinned workflow-fixtures/pipelines/ fixture set, module-level, subworkflow-level, and pipeline-level *.nf.test files snapshot different things. Do not depend on exact counts from one fixture checkout; use the split as a shape distinction and re-sample the current fixture set when making inventory claims.
0. The framework primitive
nf-test’s only assertion mechanism for content is assert snapshot(<expr>).match(). The framework serializes <expr> to JSON and writes it to a sibling <name>.nf.test.snap on first run; subsequent runs diff JSON. When <expr> contains files, nf-test serializes each as <basename>:md5,<hex> — a content hash, not a byte-equal comparison.
A .snap entry looks like:
"sarscov2 - fasta - gtf": {
"content": [{
"0": ["genome.filtered.gtf:md5,aa8b2aa1e0b5fbbba3b04d471e1b0535"],
"1": [["GTF_FILTER", "python", "3.9.5"]],
"genome_gtf": ["genome.filtered.gtf:md5,aa8b2aa1e0b5fbbba3b04d471e1b0535"]
}],
"meta": { "nf-test": "0.9.3", "nextflow": "25.10.2" },
"timestamp": "2026-02-04T14:37:34.541802092"
}
— from workflow-fixtures/pipelines/nf-core__rnaseq/modules/local/gtf_filter/tests/main.nf.test.snap.
Three flavors of <expr> cover most of the corpus:
process.out— module-level. Whole-process output map, every channel.- An ad-hoc tuple of files / scalars — module-level, selective.
snapshot(process.out.dists_multiqc, process.out.versions).match(). - A pipeline-template tuple — pipeline-level.
snapshot(removeNextflowVersion(...), getAllFilesFromDir(...), getAllFilesFromDir(..., ignoreFile: ...)).match().
The first two map cleanly to Galaxy. The third is a different problem.
1. Module-level tests (the dominant case)
Pattern (nextflow_process { ... assert snapshot(process.out).match() }). The .snap content[0] object is a map of:
- positional channel index (
"0","1", …) — every declaredoutput:line in declaration order. - named output identifier (
"genome_gtf","versions", …) — Nextflow output topics or named emissions, when declared.
Each value is a list of <basename>:md5,<hex> strings (or nested lists for collection-shaped emissions, or scalar tuples for non-file values).
1a. Direct mapping to Galaxy
| nf-test snap | Galaxy -tests.yml |
|---|---|
named channel key (genome_gtf) | outputs: label of the same role |
<basename>:md5,<hex> (single file) | checksum: md5$<hex> when preserving nf-test snapshot semantics directly; file: + compare: only when a materialized expected file is available |
[<file>, <file>, ...] (collection emission) | element_tests: keyed by element identifier; each element gets the equivalent checksum or file/assertion body |
["TOOL", "lang", "version"] (scalar tuple — e.g. versions) | drop, or has_text on tool name in a versions output if exposed |
Galaxy’s test format has two exact-output mechanisms:
checksum: "md5$<hex>"checks the produced output content by digest and does not require atest-data/expected file. This is the closest direct translation of nf-test’s<basename>:md5,<hex>snapshot token.file: test-data/<basename>pluscompare:compares the produced output against a materialized expected file. Use this when the expected bytes are available and the output format benefits fromcompareoptions (diffwithlines_diff,sim_size,contains, etc.).
Do not infer a file: comparison from a .snap token alone. The token records the expected digest, not the expected bytes.
1b. Caveat: stub-mode tests
The pattern in module tests is to pair every real test with a -stub variant:
test("sarscov2 - fasta - gtf - stub") {
options "-stub"
...
then {
assertAll(
{ assert process.success },
{ assert snapshot(process.out).match() }
)
}
}
Stub-mode runs the process’s stub: block (typically touch <output> for every declared output) instead of script:. The recorded md5 is therefore d41d8cd98f00b204e9800998ecf8427e (empty file) for every output. Stub tests are not Galaxy-translatable — Galaxy has no stub-mode equivalent in the test format. Drop them during translation; they’re a Nextflow-specific developer-loop affordance.
1c. Caveat: selective snapshots with existence pre-checks
Module tests sometimes mix snapshot-comparable and non-deterministic outputs:
{ assert path(process.out.dists_txt.get(0)).exists() },
{ assert path(process.out.pca_txt.get(0)).exists() },
{ assert snapshot(
process.out.dists_multiqc,
process.out.pca_multiqc,
process.out.versions
).match() }
— workflow-fixtures/pipelines/nf-core__rnaseq/modules/local/deseq2_qc/tests/main.nf.test.
Two patterns to map:
path(...).exists()→ Galaxy implicit by default (a labeled output that doesn’t exist fails the test). Stronger:has_size: { min: 1 }if emptiness needs to fail.- Selective
snapshot(a, b, c)→ onlya,b,cget checksum or file-backed assertions; the existence-only outputs stay as labeled outputs with nofile:.
1d. Caveat: positional vs named keys
The .snap records both positional keys ("0", "1", …) and named keys ("genome_gtf", …) for the same data. Use the named keys as the Galaxy output label source; the positional duplicates are an nf-test serialization quirk and would create false duplication in -tests.yml.
When a process declares positional-only outputs (no emit: names), the named keys are absent. In that case the Galaxy translation needs an output label invented at translation time — flag for human review.
2. Pipeline-level tests (the harder case)
Pattern (nextflow_pipeline { ... }). The nf-core pipeline template uses a near-uniform body, e.g. workflow-fixtures/pipelines/nf-core__demo/tests/default.nf.test:
def stable_name = getAllFilesFromDir(params.outdir, relative: true, includeDir: true, ignore: ['pipeline_info/*.{html,json,txt}'])
def stable_path = getAllFilesFromDir(params.outdir, ignoreFile: 'tests/.nftignore')
assertAll(
{ assert workflow.success },
{ assert snapshot(
removeNextflowVersion("$outputDir/pipeline_info/nf_core_demo_software_mqc_versions.yml"),
stable_name,
stable_path
).match() }
)
Three captures, each translatable to a different Galaxy concept (when at all):
removeNextflowVersion(...versions.yml)— the pipeline’s aggregated tool-versions YAML with the Nextflow version line stripped to avoid spurious diffs acrossnextflowversions. Galaxy has no equivalent — tool versions are tracked by wrapper<requirements>and surfaced in invocation provenance, not asserted in-tests.yml. Drop, or replace with a weakhas_texton a tool name if a versions output is exposed.stable_name(file paths, no content) — a list of paths under the publish dir. Asserts “these output files exist with these names.” In Galaxy this is the set of labeled outputs in the workflow’s outputs block, plus, for collection outputs, the set ofelement_tests:keys. There is no single Galaxy assertion that mirrors “list every published file”; the equivalent is per-output existence implied by labelling.stable_path(file paths and content via md5) — the.snaprecords<path>:md5,<hex>. Translate per-file as in §1a:checksumfor direct snapshot parity; a file-backedcompare:or assertion-family choice from planemo-asserts-idioms §1 when expected files are available or checksum is too brittle..nftignore(see §2a) controls which files are in this set.
2a. .nftignore, ignoreFile:, and ignore: globs
getAllFilesFromDir(..., ignoreFile: 'tests/.nftignore') and ignore: [<glob>, ...] prune which files appear in the snapshot. They don’t translate to Galaxy assertions — they translate to which workflow outputs the Galaxy .ga exposes as labeled outputs in the first place.
A typical .nftignore excludes:
- timestamped report HTML (MultiQC, FastQC summaries)
- log / pipeline_info bundles
versions.ymlaggregations that vary tool-by-tool
In Galaxy authoring, this maps to: don’t promote those step outputs to workflow outputs. The labeled-outputs set on the .ga is the equivalent of stable_path.
2b. succeeded_task_count
Some nf-core pipeline tests pre-template variants pass a workflow.trace.succeededCount() or similar into the snapshot. The translation is not a per-output assertion — Galaxy/Planemo invocation success is asserted at the invocation level, not per-output. Drop. (See §1 of summarize-nextflow for why summary-nextflow currently lists succeeded_task_count as a typical capture — it’s an nf-core-template convention, not nf-test framework vocabulary.)
3. Subworkflow-level tests (briefly)
Subworkflow tests (subworkflows/**/tests/main.nf.test) follow the module pattern: nextflow_workflow { ... assert snapshot(workflow.out).match() }. The .snap keys are subworkflow output channel names. Map per §1.
3a. Corpus evidence posture
This note cites local fixture paths as examples, not as stable inventory statistics. The generated Nextflow fixture set is pinned by workflow-fixtures/fixtures.yaml, but upstream test layouts can change when fixture pins move. When counts matter for a design decision, recompute them from the current fixture checkout and cite the command or survey note that produced them. For translation rules, prefer shape claims that survive fixture churn: module tests snapshot process outputs, subworkflow tests snapshot workflow outputs, and pipeline tests usually snapshot publish-directory helper results.
4. Helper functions
| Helper | Translatable? | Notes |
|---|---|---|
getAllFilesFromDir(dir, ...) | Indirectly | Defines the scope of files snapshotted; maps to which outputs the .ga exposes (§2a). No direct Galaxy assertion. |
removeNextflowVersion(file) | No | Strips a version-banner line from a YAML before hashing. Galaxy doesn’t need this — version handling is orthogonal. Drop the file from translation, or assert with has_text on a tool name. |
| Pipeline-local helpers (Groovy methods) | Case-by-case | Read the helper body. If it’s a known pattern (line strip, key prune), the translation may exist; otherwise flag for human review. |
5. What does not translate automatically
- Stub-mode tests (
options "-stub") — no Galaxy equivalent. Drop. succeeded_task_count— invocation-level signal, not a per-output assertion.- Custom Groovy helpers beyond
removeNextflowVersionandgetAllFilesFromDir. prose_assertions[]insummary-nextflow(free-text non-snapshot assertions extracted fromassertAll) — by construction free-form; needs human/LLM judgement.- Channel-emission snapshots without a published output — module tests can snapshot intermediate channels that aren’t promoted to a workflow output. Galaxy has no analog; only published outputs translate.
workflow.trace.*andworkflow.stats.*assertions — Nextflow runtime metadata, not Galaxy testable.- Test profile data overrides — these are job inputs, not assertions. Handled by nextflow-test-to-galaxy-test-plan.
6. Authoring loop
Two staircases reduce assertion-iteration cost. Both detailed in planemo-asserts-idioms §§6–7:
- Static gates:
validateTestsFile(schema) andcheckTestsAgainstWorkflow(label/type cross-check). Pure-JS, milliseconds, no Galaxy dependency. planemo workflow_test_init --from_invocationto bootstrap a-tests.ymlfrom a successful invocation, thenplanemo workflow_test_on_invocationto iterate assertions without re-running the workflow.
For a Nextflow → Galaxy translation: run the translated Galaxy workflow once, bootstrap with --from_invocation, then rewrite the autogenerated assertions using the nf-test .snap content as the expected-output authority where applicable. If the nf-test token is the only expected-output evidence, prefer checksum: "md5$<hex>"; if the expected file is materialized, choose file:/compare: or richer asserts: per planemo-asserts-idioms.
7. Source-side gap
SnapshotFixture in summary-nextflow.schema.json records captures[], helpers[], ignore_files[], ignore_globs[], snap_path — but does not parse the .snap JSON itself. For module tests where captures = ["process.out"], the structurally interesting per-channel filename+md5 lives only in the sidecar. Until that’s parsed (#142), a translation Mold has to re-read .snap at runtime.
8. Cross-references
- tests-format — Galaxy assertion vocabulary, deep-link via [[tests-format#
]]. - planemo-asserts-idioms — output-type → assertion-family decision table; tolerance magnitudes; static-gate inner loop.
- summary-nextflow — source-side schema, including
NfTestandSnapshotFixtureshape. - iwc-test-data-conventions — Galaxy job-input shape (the other half of test translation).
- iwc-shortcuts-anti-patterns — accepted-vs-smell catalog.
- component-nextflow-testing — primary-source links into nf-test, nf-core, and
nf-core/test-datasets. - nf-test snapshot docs: nf-co.re/docs/developing/testing/overview.