Skip to content

Getting started

This guide helps you to understand the concepts of nf-test and to write your first test cases. Before you start, please check if you have installed nf-test properly on your computer. Also, this guide assumes that you have a basic knowledge of Groovy and unit testing. The Groovy documentation is the best place to learn its syntax.

Let's get started

To show the power of nf-test, we adapted a recently published proof of concept Nextflow pipeline. We adapted the pipeline to the new DSL2 syntax using modules. First, open the terminal and clone our test pipeline:

# clone nextflow pipeline
git clone https://github.com/askimed/nf-test-examples

# enter project directory
cd nf-test-examples

The pipeline consists of three modules (salmon.index.nf, salmon_align_quant.nf,fastqc.nf). Here, we use the salmon.index.nf process to create a test case from scratch. This process takes a reference as an input and creates an index using salmon.

Init new project

Before creating test cases, we use the init command to setup nf-test.

//Init command has already been executed for our repository
nf-test init

The init command creates the following files: nf-test.config and the .nf-test/tests folder.

In the configuration section you can learn more about these files and how to customize the directory layout.

Create your first test

The generate command helps you to create a skeleton test code for a Nextflow process or the complete pipeline/workflow.

Here we generate a test case for the process salmon.index.nf:

# delete already existing test case
rm tests/modules/local/salmon_index.nf.test
nf-test generate process modules/local/salmon_index.nf

This command creates a new file tests/modules/local/salmon_index.nf with the following content:

nextflow_process {

    name "Test Process SALMON_INDEX"
    script "modules/local/salmon_index.nf"
    process "SALMON_INDEX"

    test("Should run without failures") {

        when {
            params {
                // define parameters here. Example:
                // outdir = "tests/results"
            }
            process {
                """
                // define inputs of the process here. Example:
                // input[0] = file("test-file.txt")
                """
            }
        }

        then {
            assert process.success
            with(process.out) {
              // Make assertions about the content and elements of output channels here. Example:
              // assert out_channel != null
            }
        }

    }

}

The generate command filled automatically the name, script and process of our test case as well as created a skeleton for your first test method. Typically you create one file per process and use different test methods to describe the expected behaviour of the process.

This test has a name, a when and a then closure (when/then closures are required here, since inputs need to be defined). The when block describes the input parameters of the workflow or the process. nf-test executes the process with exactly these parameters and parses the content of the output channels. Then, it evaluates the assertions defined in the then block to check if content of the output channels matches your expectations.

The when block

The when block describes the input of the process and/or the Nextflow params.

The params block is optional and is a simple map that can be used to override Nextflow's input params.

The process block is a multi-line string. The input array can be used to set the different inputs arguments of the process. In our example, we only have one input that expects a file. Let us update the process block by setting the first element of the input array to the path of our reference file:

when {
    params {
        outdir = "output"
    }
    process {
        """
        // Use transcriptome.fa as a first input paramter for our process
        input[0] = file("${projectDir}/test_data/transcriptome.fa")
        """
    }
}

Everything which is defined in the process block is later executed in a Nextflow script (created automatically to test your process). Therefore, you can use every Nextflow specific function or command to define the values of the input array (e.g. Channels, files, paths, etc.).

The then block

The then block describes the expected output channels of the process when we execute it with the input parameters defined in the when block.

The then block typically contains mainly assertions to check assumptions (e.g. the size and the content of an output channel). However, this block accepts every Groovy script. This means you can also import third party libraries to define very specific assertions.

nf-test automatically loads all output channels of the process and all their items into a map named process.out. You can then use this map to formulate your assertions.

For example, in the salmon_index process we expect to get one process executed and 16 files created. But we also want to check the md5 sum and want to look into the actual JSON file. Let us update the then section with some assertions that describe our expectations:

then {
    //check if test case succeeded
    assert process.success
    //analyze trace file
    assert process.trace.tasks().size() == 1
    with(process.out) {
      // check if emitted output has been created
      assert index.size() == 1
      // count amount of created files
      assert path(index.get(0)).list().size() == 16
      // parse info.json file using a json parser provided by nf-test
      def info = path(index.get(0)+'/info.json').json
      assert info.num_kmers == 375730
      assert info.seq_length == 443050
      assert path(index.get(0)+'/info.json').md5 == "80831602e2ac825e3e63ba9df5d23505"
    }
}

The items of a channel are always sorted by nf-test. This provides a deterministic order inside the channel and enables you to write reproducible tests.

Your first test specification

You can update the name of the test method to something that gives us later a good description of our specification. When we put everything together, we get the following full working test specification:

nextflow_process {

    name "Test Process SALMON_INDEX"
    script "modules/local/salmon_index.nf"
    process "SALMON_INDEX"

    test("Should create channel index files") {

        when {
            process {
                """
                input[0] = file("${projectDir}/test_data/transcriptome.fa")
                """
            }
        }

        then {
            //check if test case succeeded
            assert process.success
            //analyze trace file
            assert process.trace.tasks().size() == 1
            with(process.out) {
              // check if emitted output has been created
              assert index.size() == 1
              // count amount of created files
              assert path(index.get(0)).list().size() == 16
              // parse info.json file
              def info = path(index.get(0)+'/info.json').json
              assert info.num_kmers == 375730
              assert info.seq_length == 443050
              assert path(index.get(0)+'/info.json').md5 == "80831602e2ac825e3e63ba9df5d23505"
            }
        }
    }
}

Run your first test

Now, the test command can be used to run your test:

nf-test test tests/modules/local/salmon_index.nf.test --profile docker

Specifying profiles

In this case, the docker profile defined in the Nextflow pipeline is used to execute the test. The profile is set using the --profile parameter, but you can also define a default profile in the configuration file.

Congratulations! You created you first nf-test specification.

Nextflow options

nf-test also allows to specify Nextflow options (e.g. -dump-channels, -stub-run) globally in the nf-test.config file or by adding an option to the test suite or the actual test. Read more about this in the configuration documentation.

nextflow_process {

    options "-dump-channels"

}

What's next?

  • Learn how to write assertions
  • Learn how to write workflow tests (integration test or e2e)
  • Learn how to config nf-test