Cucumber Step Definitions

Let's add step definition code that binds Cucumber specifications to actual code and will perform all the actions for each test.

Cucumber Step Definitions
Photo by Luca Bravo / Unsplash

In the last post, I added Cucumber to my Kotlin project which is managed by Gradle and began to add some features, scenarios and steps. When we ran the Cucumber CLI through a plugin, Cucumber for Java, it gave some helpful feedback to show what was missing, namely some sample step definitions that match the feature that we wrote.

A Closer Look at the Output

Let's take one of the statements that Cucumber evaluated: When the application is run with argument "Dave". It suggests the following Java code should be written to bind this step to code:

@When("the application is run with argument {string}")
public void the_application_is_run_with_argument(String string) {
    // Write code here that turns the phrase above into concrete actions
    throw new io.cucumber.java.PendingException();
}

What is useful here is that the variable in the input statement, “Dave”, has been extracted as a variable to the method definition. This will allow the code to be run with the appropriate arguments, and there could be several steps with different arguments passed to the same function. This is the case for another of the steps. Both Then the "Hello World!" is output to the console and Then the "Hello Dave!" is output to the console have the following code suggestion:

@Then("{string} is output to the console")
public void is_output_to_the_console(String string) {
    // Write code here that turns the phrase above into concrete actions
    throw new io.cucumber.java.PendingException();
}

There is an alternative to the first suggested method as written above. Because the @When has the single word “Dave” in it, we could also use the variable type word in its place. The annotation could be replaced with @When("the application is run with argument {word}"), but note that the step in the feature file also needs to be changed to remove the quotation marks. Cucumber needs quotation marks to recognise a string with spaces, but a word can be picked out on its own.

Adding the First Step Definition

Cucumber for Java has suggested the code as Java but we want Kotlin. IntelliJ can do a simple conversion – all you have to do is paste the Java code into a Kotlin file and it will offer to change it to the target language. (Note this works both ways and can be a great technique if you are learning to learn to program in Kotlin having learned Java.)

Now that the code is there we can add some details. The first function, Given the helloworld application is where we are going to setup the HelloWorld class and its dependencies. This object will need to be available in the @When step to run the test and the dependencies will need to be available in the  @Then step to check the results.

The dependencies that are needed will give us a means to capture the output that is sent to the console. As described in Testing Side Effects, this will be done by setting up an alternate PrintStream in the application when it is run as a test. Let's define the variables at class level as:

private lateinit var byteArrayOutputStream: ByteArrayOutputStream
private lateinit var printStream: PrintStream
private lateinit var helloWorld: HelloWorld

Now the variables can be instantiated in the first function that we'll implement.

@Given("the helloworld application")
fun helloworld_application() {
    byteArrayOutputStream = ByteArrayOutputStream()
    printStream = PrintStream(byteArrayOutputStream)
    helloWorld = HelloWorld(printStream)
}

This brings up a slight issue with using Cucumber and Kotlin. I would rather define all variables using the immutable val definition but that means I am forced to instantiate them at startup time. Objects to use during the test get set up in functions marked with @Given annotations, but must be accessible to other functions, those annotated with @When to run the test and @Then to check for the results.

In this case, there is nothing specific being passed from the scenario description to the step definition, so I could have simply declared these as val variables at class level. Usually there is some variable passed in the @Given steps, however, and the instantiation must happen with the function.

So it is not going to be straightforward to make the variables immutable, defined with the val keyword. Since this is test code, and the context is limited to this single class, I think that is something I can live with, even if it makes me uncomfortable. I'll return to this if I can figure out a nicer way to keep Kotlin's immutable-by-default approach.

Running and Checking

Let's try running the Cucumber tests. Note that there is one more configuration step required before this will work. Cucumber for Java is not smart enough to find the step definitions. Another small piece of configuration is required. Edit the runtime configuration by opening “Edit Configurations...” from the Run menu. Enter the package name of the step definition class into the “Glue” field; in my case this is com.failedtofunction.cucumbersample.

Having run the scenarios again, the Cucumber output no longer gives out about the @Given function having recognised that one of the step definition now matches. There are still three step definitions that need to be defined.

Add the Remaining Step Definitions

The @When functions are used to do the action that we want to test. In this example that is to call helloWorld. I've chosen to use the word Cucumber type here as described above. In this piece of code the helloWorld object that we set up in the @Given function is interacted with, passing the name specified in the scenario.

    @When("the application is run with argument {word}")
    fun the_main_class_is_run_with_argument_dave(name: String) {
        helloWorld.helloWorld(arrayOf(name))
    }

The other test case is calling the function with no argument. It can be done with the following code:

    @When("the application is run with no arguments")
    fun the_main_class_is_run_with_no_arguments() {
        helloWorld.helloWorld(emptyArray())
    }

Finally the @Then function is going to check that the expected behaviour occurred. Generally assertions will be placed in this sort of function.

    @Then("{string} is output to the console")
    fun the_is_output_to_the_screen(text: String?) {
        val result = String(byteArrayOutputStream.toByteArray()).trim()
        assertEquals(text, result)
    }

The Feature should now pass all scenarios. That's basically all there is to adding Cucumber behavioural tests to a Kotlin project that uses Gradle for build configuration.

What's Left?

There are a few other things to tidy up. Firstly the Cucumber features will not get run when the Gradle build runs, not without a little more configuration. That would mean relying on developers to run the tests manually after making changes – something that they are likely to forget to do. So making sure that the tests can be run with the unit tests is something I want to cover next time.

Another point is that the tests are supposed to provide living documentation for the project, but at the moment all the features and scenarios are hidden inside the source control for the project. Business people that want to see what scenarios exist should not need to look through source code repositories, in my opinion, even if they have the right permissions to do so. Wouldn't it be better if the results of running the features was published somewhere so that the current state of the project could be viewed by everyone in the organisation?

what’s going on here
Photo by John Schnobrich / Unsplash