Test definitions
Adding tests to your subworkflow is a critical step to ensure it works correctly and continues to do so as the codebase
evolves. The nf-core
command automatically generates the test infrastructure when creating a subworkflow. Inside the
tests directory of your subworkflow, you’ll find main.nf.test, which contains inputs, test cases, and assertion
instructions for nf-test, the Nextflow test framework. Open it and follow through the rest of
this section.
Enable stubbing
Section titled “Enable stubbing”Each module comes with it’s stub. To activate stubbing for the subworkflows, add the following tag
and options
to the nextflow_workflow
block :
nextflow_workflow { name "Test Workflow <SUBWORKFLOW_NAME>" script "../main.nf" ...
tag "stub" options "-stub-run" ...}
Setup before tests
Section titled “Setup before tests”Setup sections are used to prepare data before test cases are run, to download data, prepare algorithms and generate awaited
return values. In nf-neuro, test datasets are provided via the LOAD_TEST_DATA
subworkflow (refer to this section to learn how to find packages).
Once you’ve selected the archives and files needed, add a setup
section before the test section. Replace the <archive>
with the name of the one you need and you’ll be able to access its content within your test suite :
setup { run("LOAD_TEST_DATA", alias: "LOAD_DATA") { script "../../../../subworkflows/nf-neuro/load_test_data/main.nf" workflow { """ input[0] = Channel.from( [ "<archive>" ] ) input[1] = "test.load-test-data" """ } }}
Define test cases
Section titled “Define test cases”Test cases for subworkflows are defined each in their own test block:
test("example - test") { when{ workflow { ... } } then{ ... }}
A block minimally contains a when
(what to test) and a then
(what to assert). Inside of them, no need to define the
name of the subworkflow being tested, or import it. nf-test
will take care of that for you when the test will be run,
through the workflow
placeholder inside the when
block.
Define test inputs
Section titled “Define test inputs”The only job of the when
block is to define the inputs to supply to your subworkflow. You do so by adding a list of
inputs inside the workflow
block, as follows:
test("example - test") { when { workflow { """ input[0] = LOAD_DATA.out.test_data_directory.map{ test_data_directory -> [ [ id:'test1', single_end:false ], // meta map // -> meta file("\${test_data_directory}/image.nii.gz"), // -> image file [] // -> an optional input, not provided ], [ [ id:'test2', single_end:false ], // meta map // -> meta file("\${test_data_directory}/image.nii.gz"), // -> image file file("\${test_data_directory}/mask.nii.gz") // -> an optional input, provided this time ] } """ } } then { ... }}
Define assertions
Section titled “Define assertions”The assertions for a subworkflow have to finely inspect the structure and shape of the output data of the subworkflow, without accessing the data itself. The only file required to be snapshotted is the versions.yml file that keeps track of dependencies :
then { assertAll( { assert workflow.success }, { assert snapshot( workflow.out .findAll{ !it.key.isInteger() && it.value } .collect{ item -> ["versions"].contains(item.key) ? item.value : file(item.value.get(0).get(1)).name } ) }, { assert workflow.out .findAll{ !it.key.isInteger() } .every{ channel -> channel.value.every{ item -> item instanceof ArrayList ? item.get(1) : item } } } )}
If the content of the channels outputed by the subworkflow can vary with configuration, then output channels have to be
further verified to validate either the absence or presence of data in them. Given a list of channels names
[ch_A, ch_B, ...]
that should not produce outputs, use the following assertion :
then { assertAll( ..., { assert workflow.out .findAll{ !it.key.isInteger() } .every{ channel -> ["ch_A", "ch_A", ...].contains(channel.key) ? channel.value.size() == 0 : channel.value.every{ item -> item instanceof ArrayList ? item.get(1) : item } } } )}
Configure the subworkflow
Section titled “Configure the subworkflow”To configure parameters of the subworkflow, use the config
parameter, either globally or at the scope of the test case,
to import a nextflow.config file.
Generate tests snapshots
Section titled “Generate tests snapshots”Once you have correctly setup your test cases and made sure the data is available, the test subworkflow has to be
pre-tested so output files that gets generated are snapshotted correctly before being pushed to nf-neuro
.
To do so, run:
nf-core subworkflows test -u <subworkflow_name>
All the test cases you defined will be run, watch out for errors! Once everything runs smoothly, look at the snapshot file produced at tests/main.nf.test.snap in your subworkflow’s directory and validate that ALL outputs produced by test cases are caught. Their md5sum is critical to ensure future executions of your test produce valid outputs.
A complete example for a subworkflow
Section titled “A complete example for a subworkflow”Here’s an example of a complete test for a hypothetical subworkflow that preprocesses imaging data:
nextflow_workflow {
name "Test Workflow PREPROCESSING" script "../main.nf" workflow "PREPROCESSING" config "./nextflow.config"
tag "subworkflows" tag "subworkflows/preprocessing" tag "subworkflows/load_test_data"
tag "stub" options "-stub-run"
setup { run("LOAD_TEST_DATA", alias: "LOAD_DATA") { script "../../../../subworkflows/nf-neuro/load_test_data/main.nf" workflow { """ input[0] = Channel.from( [ "raw_b0.zip", "raw_segmentation.zip" ] ) input[1] = "test.load-test-data" """ } } }
test("preprocessing - standard") { when { workflow { """ ch_split_test_data = LOAD_DATA.out.test_data_directory .branch{ b0: it.simpleName == "raw_b0" segmentation: it.simpleName == "raw_segmentation" } ch_images = ch_split_test_data.b0.map{ test_data_directory -> [ [ id:'test', subject:'01' ], file("\${test_data_directory}/b0.nii.gz") ] } ch_masks = ch_split_test_data.segmentation.map{ test_data_directory -> [ [ id:'test', subject:'01' ], file("\${test_data_directory}/brainmask/slices/axial.nii.gz") ] } input[0] = ch_images input[1] = ch_masks """ } } then { assertAll( { assert workflow.success }, { assert snapshot( workflow.out .findAll{ !it.key.isInteger() && it.value } .collect{ item -> ["versions"].contains(item.key) ? item.value : file(item.value.get(0).get(1)).name } ) }, { assert workflow.out .findAll{ !it.key.isInteger() } .every{ channel -> channel.value.every{ item -> item instanceof ArrayList ? item.get(1) : item } } } ) } }}