Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support step definitions written in Kotlin #1829

Open
mpkorstanje opened this issue Nov 26, 2019 · 21 comments
Open

Support step definitions written in Kotlin #1829

mpkorstanje opened this issue Nov 26, 2019 · 21 comments
Labels
🙏 help wanted Help wanted - not prioritized by core team ⚡ enhancement Request for new functionality

Comments

@mpkorstanje
Copy link
Contributor

mpkorstanje commented Nov 26, 2019

Summary

Cucumber currently supports Kotlin by using Java Annotations or Lambda based step definitions. This works because Java and Kotlin can interoperate. This does however fails to leverage some of the advantages of Kotlin (#1554, #1520). So a Kotlin backend would be most useful.

Update:

The approach describe below will not work in the future because we are aiming for a design where step definitions must be discoverable without instantiating the glue class. For this reason we are deprecating cucumber-java8 (#2174) and this design was patterned on that. As a replacement for cucumber-java8 we are considering cucumber-lambda (#2279). This may or may not make this ticket obsolete.

Goals

  1. Implement a Kotlin backend that supports step definitions, hooks, data table and parameter type declaration.

  2. Avoid the duplication of method name and step definition common when using annotation based step definitions. Example:

Given("a step definition")
public void given_a_step_definition() {}
  1. Use Kotlin primitives and types everywhere. This should include Cucumber expressions, parameter types, data table types, DataTable and DocString.

  2. Generate Kotlin step definitions

Step definitions

class StepDefinitions(belly: Belly) : En {
   init {
     Given("there are {int} cukes in my belly"){ cukes : Int -> 
         belly.setCukes(cukes)
     }

      When("^I eat ([0-9]) cukes$") { cukes : Int ->
         belly.addCukes(cukes)
     }
   }
}

En would be implement as a class with members for various keywords and other glue (see java8 equivalent).

interface Test {

    fun Given(expression: String, f: Function<Unit>) {
        //Register with thread local as in Java 8
    }
    
    ... other key words
}

Note that Function has subtypes that support up to up to 22 parameters. After that it becomes a varg and we can no longer user f.javaClass.methods[1].genericParameterTypes to work out what the the types were. As a result Cucumber won't be able to convert arguments automatically when using regular rather then Cucumber expressions. This is a reasonable drawback for wanting to use 22+ parameters.

Steps to take

  1. Implement a KotlinBackendProviderService and KotlinBackend and KotlinStepDefinition for a single annotation. For examples see Java8** equivalents in the java8 module.
  2. Experiment to see how the interop works
  3. Implement everything else (sorry about the draw-the-rest-of-the-owl, I don't know what or how yet).
@mpkorstanje mpkorstanje added ⚡ enhancement Request for new functionality Kotlin 🙏 help wanted Help wanted - not prioritized by core team labels Nov 26, 2019
@bodiam
Copy link

bodiam commented Nov 26, 2019

Hi @mpkorstanje , I'm sorry but I think you can forget about the arbitrary strings as method names feature. Kotlin doesn't really allow arbitrary strings, it just allows spaces, but it still has to adhere to the JVM spec (letters, numbers, and currency symbols. No [, ] ^ { } etc...)

@mpkorstanje
Copy link
Contributor Author

mpkorstanje commented Nov 26, 2019

Ah that is too bad. Then we're back to using lambda step definitions. Changed the ticket to match that.

@bodiam
Copy link

bodiam commented Nov 26, 2019 via email

@mpkorstanje
Copy link
Contributor Author

mpkorstanje commented Nov 26, 2019

Both are valid options.

We can either use lambdas:

class StepDefinitions(belly: Belly) : En {
   init {
     Given("there are {int} cukes in my belly"){ cukes : Int -> 
         belly.setCukes(cukes)
     }
   }
}

Or we can use annotations:

class StepDefinitions(belly: Belly) {
  
   @Given("there are {int} cukes in my belly")
   fun there_are_cukes_in_my_belly(cukes : Int){
       belly.setCukes(cukes)
   }
}

Neither is perfect. The lambdas must be declared in the init, the annotations have the the repetition of the method name and cucumber expression.The annotations are also a bit simpler to implement then the lambdas but we have solved that problem before so I would consider that unimportant.

Additionally the main hurdle for cucumber-java8 was type erasure. A lambda always loses its generic type information and we have to fish it out of the constant pool.

    init {
        val Step = fun(expression: String, f: Function<Unit>) {
             
        }
        Step("{int} cukes in my {string}") { number: Int, thing: String, value : List<String> ->

        }
    }

However in this example f.javaClass.methods[1].genericParameterTypes[2] as ParameterizedType will result in a java.util.List<java.lang.String> so Kotlin doesn't lose the type info.

@pavelpp
Copy link

pavelpp commented Jan 27, 2020

Any progress on this? It's a major showstopper as it's hard to sell to dev team Kotlin-Cucumber combo without proper kotlin support.

@aslakhellesoy
Copy link
Contributor

@pavelpp is the show stopper major enough that you or someone from your team would be willing to help us implement this?

What questions do you have to get started?

@pavelpp
Copy link

pavelpp commented Jan 27, 2020

@pavelpp is the show stopper major enough that you or someone from your team would be willing to help us implement this?

What questions do you have to get started?

@aslakhellesoy sure, I'd even contribute, if it is not way over my head. Where do I start?

@mpkorstanje
Copy link
Contributor Author

mpkorstanje commented Jan 27, 2020

It's a major showstopper as it's hard to sell to dev team Kotlin-Cucumber combo without proper kotlin support.

@pavelpp Kotlins interop with Java i quite good and people are using cucumber-java with Kotlin in real world scenarios. What showstopper issues are you running into exactly?

@pavelpp
Copy link

pavelpp commented Jan 27, 2020

@mpkorstanje Main showstopper is inability to auto-generate step definition snippets using shortcut keys. The fact that I need to actually type a whole bunch of boilerplate code. In the past devs were also scared off by all the regexes in step definitions and when I told them it's auto-generated they were ready to accept. Now, after migration to Kotlin (POC for now) I can not give them this luxury and have to explain that they need to type everything manually. That is not going to fly.

@mpkorstanje
Copy link
Contributor Author

Main showstopper is inability to auto-generate step definition snippets using shortcut keys.

Do you mean auto-generated snippets by an IDE?

@pavelpp
Copy link

pavelpp commented Jan 27, 2020

@mpkorstanje yes. I guess this is actually done by IDE plugin?

@mpkorstanje
Copy link
Contributor Author

mpkorstanje commented Jan 27, 2020

It depends on what you are referring too.

When Cucumber executes and encounters undefined steps it will print a snippet. Currently these snippets are in Java not Kotlin (#1520). This happens at runtime. IDE's provide a similar functionality and will do a static analysis to generate code when steps are undefined.

It is also worth noting that InteliJ IDEA will conver the Java snippets to Kotlin when pasted.

@pavelpp
Copy link

pavelpp commented Jan 27, 2020

@mpkorstanje I meant IDE. When I hit ALT+Enter in IDEA it suggests to create step definition for given step. Works for Java, but not for Kotlin.

@mpkorstanje
Copy link
Contributor Author

You'll have to ask IDEA to support that unfortunately.

@maio
Copy link

maio commented Jun 26, 2020

We've been using cucumber-java in Kotlin for a year or so, and have quite a few scenarios in multiple projects. In IDEA we just copy auto-generated Java snippets from test output into Kotlin code and IDEA converts them into Kotlin automatically.

It would be nice to use Alt+Enter, but this workflow is pretty close. BTW it's probably missing feature of Cucumber IDEA plugin not cucumber-java?

@stale
Copy link

stale bot commented May 20, 2021

This issue has been automatically marked as stale because it has not had recent activity. It will be closed in two months if no further activity occurs.

@stale stale bot added the ⌛ stale Will soon be closed by stalebot unless there is activity label May 20, 2021
@hakanai
Copy link

hakanai commented Jul 17, 2021

In Kotlin I think I would prefer to write something like this:

class StepDefinitions(belly: Belly) {
  
   @Given
   fun `there are {int} cukes in my belly`(cukes : Int){
       belly.setCukes(cukes)
   }
}

Is it as simple as saying that if the description is not provided, the name of the method is used as the description?

@stale stale bot removed the ⌛ stale Will soon be closed by stalebot unless there is activity label Jul 17, 2021
@mpkorstanje
Copy link
Contributor Author

Is it as simple as saying that if the description is not provided, the name of the method is used as the description?

Unfortunately not. See: #1829 (comment)

@hakanai
Copy link

hakanai commented Jul 17, 2021

That's unfortunate. I'd still prefer to write that for all the cases where it is possible, but it sucks that Java doesn't just let us use any character in a method name. (At the bytecode level, why should they care?)

@mpkorstanje
Copy link
Contributor Author

This looks promising:

package io.cucumber.kotlin

import kotlin.reflect.KClass

class World {

    fun hello() {
        println("Hello world!")
    }

    fun hello(number: Int) {
        println("Hello world ${number}!")
    }
}

val stepDefinitions = define {
    using<World> {
        step("hello world") {
            hello();
        }
        step("hello again world {int}") { number: Int ->
            hello(number)
        }
    }
}

The plumbing:

fun define(define: StepDefinitions.() -> Unit): StepDefinitions {
    val stepDefinitions = StepDefinitions()
    stepDefinitions.define()
    return stepDefinitions;
}

class StepDefinitions {
    val stepDefinitions: MutableList<Any> = mutableListOf()
    inline fun <reified T : Any> using(define: Using<T>.() -> Unit) {
        val context = Using(T::class)
        context.define()
        stepDefinitions.addAll(context.stepDefinitions);
    }
}

class Using<T : Any>(val kls: KClass<T>) {
    val stepDefinitions: MutableList<Any> = mutableListOf()
    inline fun step(expression: String, noinline implementation: T.() -> Unit) {
        stepDefinitions.add(StepDefinition00(expression, implementation, kls))
    }

    inline fun <reified V1 : Any> step(expression: String, noinline implementation: T.(a: V1) -> Unit) {
        stepDefinitions.add(StepDefinition01(expression, implementation, kls, V1::class))
    }
}

data class StepDefinition00<T : Any>(
    val expression: String,
    val implementation: T.() -> Unit,
    val executionContextType: KClass<T>
)

data class StepDefinition01<T : Any, V1 : Any>(
    val expression: String,
    val implementation: T.(V1) -> Unit,
    val executionContextType: KClass<T>,
    val expressionArgType01: KClass<V1>
)

fun main() {
    stepDefinitions.stepDefinitions.forEach { println(it) }
}

@stale
Copy link

stale bot commented Apr 14, 2023

This issue has been automatically marked as stale because it has not had recent activity. It will be closed in two months if no further activity occurs.

@stale stale bot added the ⌛ stale Will soon be closed by stalebot unless there is activity label Apr 14, 2023
@mlvandijk mlvandijk removed the ⌛ stale Will soon be closed by stalebot unless there is activity label Jul 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🙏 help wanted Help wanted - not prioritized by core team ⚡ enhancement Request for new functionality
Projects
None yet
Development

No branches or pull requests

7 participants