skills/nextflow/references/testing.md
nf-test is the standard test framework for Nextflow. nf-core requires nf-test coverage for every module, subworkflow, and pipeline. Sources: https://www.nf-test.com and https://nf-co.re/docs/developing/testing/overview
# install (one of)
conda install -c bioconda nf-test
curl -fsSL https://get.nf-test.com | bash
nf-test init # creates nf-test.config + tests/ scaffolding in a project
Test files end in .nf.test and live next to the component (tests/main.nf.test). Expected results are stored in a sibling .nf.test.snap snapshot file.
Three test scopes match what you're testing:
nextflow_process — a single process/modulenextflow_workflow — a (sub)workflownextflow_pipeline — a whole pipeline (main.nf)Common layout:
nextflow_process {
name "Test SAMTOOLS_SORT"
script "../main.nf" // the module under test
process "SAMTOOLS_SORT"
tag "modules"
tag "modules_nfcore"
tag "samtools"
tag "samtools/sort"
test("sarscov2 - bam") {
when {
process {
"""
input[0] = [
[ id:'test', single_end:false ],
file(params.modules_testdata_base_path + 'genomics/sarscov2/illumina/bam/test.bam', checkIfExists: true)
]
"""
}
}
then {
assertAll(
{ assert process.success },
{ assert snapshot(process.out).match() }
)
}
}
}
input[0], input[1], … bind positional process inputs.setup { } block can run prerequisite processes to produce inputs.params { } block sets parameters; a config "..." line loads a config for the test.options "-stub" inside a test(...) block to exercise the stub: script.The when block supplies inputs; the then block asserts on results. Use a setup block when a module needs another module's output first:
test("sort then index") {
setup {
run("SAMTOOLS_SORT") {
script "../../sort/main.nf"
process {
"""
input[0] = [ [id:'test'], file(params.test_data + 'test.bam', checkIfExists:true) ]
"""
}
}
}
when {
process {
"""
input[0] = SAMTOOLS_SORT.out.bam
"""
}
}
then {
assert process.success
assert snapshot(process.out).match()
}
}
Inside then, wrap multiple checks in assertAll(...) so all failures are reported at once. Useful handles and helpers:
| Expression | Checks |
|---|---|
process.success / process.failed | Task completed / failed |
process.exitStatus == 0 | Exit code |
process.out.<emit> | A named output channel's contents |
process.out.bam.get(0) | First emitted item |
workflow.success, workflow.trace.tasks().size() | Workflow outcome / task count |
path(process.out.bam[0][1]).exists() | A file exists |
snapshot(...).match() | Compare to stored snapshot |
assertContainsInAnyOrder(ch, [...]) | A channel contains the given items (order-agnostic) |
There is no
assertContainsInOrder. For ordered or substring checks on file contents, read the lines and assert directly, e.g.assert path(out[0][1]).readLines().any { it.contains('Done') }orassert path(out[0][1]).readLines().last().contains('completed').
Plugins extend assertions for domain files (e.g. nft-bam for BAM, nft-vcf for VCF, nft-utils); nf-core enables these in nf-test.config. Example with a file-content assertion:
then {
assertAll(
{ assert process.success },
{ assert path(process.out.bam[0][1]).exists() },
{ assert snapshot(
bam(process.out.bam[0][1]).getSamLinesMD5(),
process.out.versions
).match() }
)
}
snapshot(x).match() serializes x and compares it to the .nf.test.snap file. The first run records the snapshot; later runs fail if output changes.
versions.yml, list sizes — not absolute paths or timestamps.nf-test test --update-snapshot..match("bam"), .match("versions").nextflow_pipeline {
name "Test full pipeline"
script "../main.nf"
test("default params") {
when { params { outdir = "$outputDir"; input = "tests/samplesheet.csv" } }
then {
assert workflow.success
assert workflow.trace.succeeded().size() > 0
}
}
}
Use tiny inputs from nf-core/test-datasets (nf-core test-datasets search ...) so tests stay fast.
nf-test test # run everything
nf-test test modules/nf-core/samtools/sort/ # a directory
nf-test test tests/main.nf.test # one file
nf-test test --tag samtools # by tag
nf-test test --profile docker # choose container engine
nf-test test --update-snapshot # accept new snapshots
nf-test test --changed-since HEAD^ # only components changed since a ref
nf-test test --only-changed --ci # CI mode: fail (don't write) on a missing snapshot
nf-test.config sets the test directory, default profile, and plugins. CI typically runs only changed components via --changed-since plus the nf-core nf-test GitHub Action.
For nf-core components, prefer the wrapper commands — they run nf-test with the correct profiles, tags, and snapshot handling, and create scaffolds the test files:
nf-core modules create mytool # scaffolds tests/main.nf.test
nf-core modules test mytool # runs the module's nf-test suite
nf-core subworkflows test mysubwf
Every nf-core module/subworkflow must ship passing nf-test tests (including a stub test) with committed snapshots; nf-core modules lint checks their presence. See references/developing.md.