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

Consider cucumber-java-lambda as a replacement for cucumber-java8 #2279

Open
mpkorstanje opened this issue Apr 3, 2021 · 30 comments · May be fixed by #2542
Open

Consider cucumber-java-lambda as a replacement for cucumber-java8 #2279

mpkorstanje opened this issue Apr 3, 2021 · 30 comments · May be fixed by #2542
Assignees
Labels
⚡ enhancement Request for new functionality
Milestone

Comments

@mpkorstanje
Copy link
Contributor

mpkorstanje commented Apr 3, 2021

cucumber-java8 allows step definitions to be defined as lambda's. This is really nice because it removes the need to type your step definition twice as you would with cucumber-java. So there is a good reason to use lambda's to define step definitions.

Compare:

Given("A gherkin and a zukini", () -> { 

});

@Given("A gherkin and a zukini")
public void a_gherkin_and_zukini(){

}

Unfortunately with cucumber-java8 lambda's must be defined in the constructor of a step definition class. As a result we can not know which step definitions are defined until a Cucumber scenario has started and all world objects are instantiated. This makes it impossible to discover, cache and validate step definitions up front, preventing us from making Cucumber more efficient (#2035).

public class StepDefinitions {
     public StepDefinitions(){
        Given("A gherkin and a zukini", () -> { 

       });
     }
}

Additionally Cucumber uses typetools to determine the type of lambda parameters. This requires the use of of Unsafe to fish in the constant pool. This is a non-trivial process and Cucumber currently uses typetools to facilitate this. However because this fundamentally depends on unsafe operation it is not guaranteed to work in the long run.

Requested solution

  1. Implement cucumber-lambda as an alternative for cucumber-java8 that uses a DSL to build step definitions. Because this DSL is created in a static field it can be discovered in the same way cuucmber-java discovers step definitions and avoids the issues of cucumber-java8.
public class CucumberLambdaStepDefinitions {

    @Glue
    public static CucumberLambda glue = CucumberLambda
        .using(World.class)
        .step("A gherkin and a zukini", (World world) -> () -> { 
             world.setGherkins(1);
             world.setZukinis(1);
        })
        .step("{int} gherkin(s) and {int} zukini(s)", (World world) -> (int gherkins, int zukinis) -> {
             world.setGherkins(gherkins);
             world.setZukinis(zukinis);
        });

       // repeat for hooks, parameter types and data table types, ect
       // hooks should be named beforeAll, beforeEach, beforeStep.
  1. Avoid the use of typetools where possible by specifying all parameter types

  2. The World object is created using DI as usual. Consider the possibility of defining steps/hooks using multiple objects.

CucumberLambda
    .using(GherkinPatch.class, ZukiniPatch.class)
    .step("A gherkin and a zukini", (gherkinPatch, zukiniPatch) -> () -> { 
        // tend to the vegetable garden here
    });

Out of scope

  1. Generate localized vairations of the DSL that use Given/When/Then.
    @Glue
    public static CucumberLambda glue = io.cucumber.lambda.en.CucumberLambda
        .using(World.class)
        .given("A gherkin and a zukini", (World world) -> () -> { 
             world.setGherkins(1);
             world.setZukinis(1);
        })
        .when("{int} gherkin(s) and {int} zukini(s)", (World world) -> (int gherkins, int zukinis) -> {
             world.setGherkins(gherkins);
             world.setZukinis(zukinis);
        });
        // ect.
@mpkorstanje mpkorstanje added ⚡ enhancement Request for new functionality 🙏 help wanted Help wanted - not prioritized by core team labels Apr 3, 2021
@laeubi
Copy link

laeubi commented Apr 4, 2021

I must confess I always found the annotation method more powerful, readable and easy to use. Its also easier for tools to handle them and give good advice. Code is written once but read hundreds of times and I never have found it a burden to "type twice" (better read as: Think twice :-)

Especially the more advanced one I hardly see any "easier typing" but chances for people getting crazy getting all this ()-> {} -> (...) right 👎

@aslakhellesoy
Copy link
Contributor

I agree with @laeubi - I always preferred the annotation style. At the time when we wrote the lambda API, lambdas were relatively new in Java, and I think the main driver was novelty.

I love lambdas for functional programming, but step definitions feel more "procedural" to me, which is probably why I never used them myself.

That said, I think the API you've proposed looks really nice. Maybe we should gauge the interest in the community with a survey? I'd like to understand more about what people want. If people prefer lambdas, what are the main reasons?

@mpkorstanje
Copy link
Contributor Author

mpkorstanje commented Apr 4, 2021

Looking at the monthly stats I think there is already sufficient interest in lambda's:

io.cucumber:cucumber-core:   800k downloads
io.cucumber:cucumber-java:   700k downloads
io.cucumber:cucumber-junit:  600k downloads
io.cucumber:cucumber-spring: 200k downloads
io.cucumber:cucumber-java8:  160k downloads
io.cucumber:cucumber-testng: 160k downloads
io.cucumber:cucumber-pico:   140k downloads
io.cucumber:cucumber-guice:   20k downloads

--

One of the interesting things is also that less then half the users do not use dependency injection. This means that information is shared between steps using static variables. This is somewhat understandable because when using annotations you would either have to put all step definitions in one file, use dependency injection or use static variables.

The use of static variables is most undesirable as it makes it likely that tests influence and each other and makes parallel execution of tests impossible. However the solution, dependency injection, has a significant conceptual on ramp. This means that many users of Cucumber who don't have a significant experience in software engineering concepts will not know about this. Furthermore the concept is also hidden, making discovery impossible.

This is quite unlike the other Cucumber implementations where the shared context (world) is an explicit concept. And in other Cucumber implementations this shared context can be used even without dependency injection. By using a DSL which makes the shared context (world) explicit it would be possible for users to organize their step definitions in different files and share information between steps without using dependency injection. This should provide a better way to keep tests clean and smooth the on-ramp to both parallel execution and dependency injection significantly.

--

Compared to annotations a DSL with lambdas does solve another problem. When using annotations without dependency injection steps can only access the class in which they are defined. This puts constraints on the organizations of step definitions.

For example suppose we have a process that involves composting some vegetables with the aid of cow manure and dung beetles. We could organize these steps around the components they interact with along with other steps that interact with the same components.

public class VegtablePatchStepDefinitions { 
    private final VegtablePatch vegtablePatch;

    @Given("A gherkin and a zukini")
    public void a_gherkin_and_zukini(){
         // can only access members of VegtablePatchStepDefinitions directly
    }
}
public class CompostStepDefinitions { 
 
    private final CompostHeap heap;
    private final DungBeetleBreeder dungBeetleBreeder;
    private final Cows cows;

    @Given("A hand full of dung beetles")
    public void a_hand_full_of_dung_beetles(){
         // can only access members of CompostStepDefinitions directly
    }
}

Or we could organize them thematically with a lot less ceremony:

public class CreatingVegtableCompost {

    @Glue
    public static CucumberLambda glue = CucumberLambda
        .using(VegtablePatch.class)
        .step("A gherkin and a zukini", (vegtablePatch) -> () -> { 
             
        })
        .using(CompostHeap.class, DungBeetleBreeder.class, Cows.class)
        .step("A hand full of dung beetles", (compostHeap, dungBeetleBreeder, cows) -> () -> {

        });
}

@mpkorstanje mpkorstanje changed the title Implement cucumber-lambda as a replacement for cucumber-java8 Consider cucumber-lambda as a replacement for cucumber-java8 Apr 4, 2021
@rmannibucau
Copy link
Contributor

@mpkorstanje think the stats are biased because libs integrate with core and not spring/cdi/whatever framework in general. For example, cukespace will provide IoC on all env without using cucumber-cdi but using core and java8 (for lambda support).
If CucumberLambda can be "not" static then it would enable to rely on injections properly (and therefore state). It was a blocker we needed to workaround in cukespace for lambda support adding a "init" lifecycle for the lambdas.

@mpkorstanje
Copy link
Contributor Author

mpkorstanje commented Apr 4, 2021

@mpkorstanje think the stats are biased because libs integrate with core and not spring/cdi/whatever framework in general.

If 50% of all cucumber users were using some DI container that wasn't provided by Cucumber I think we would have known a few of them.

If CucumberLambda can be "not" static then it would enable to rely on injections properly (and therefore state). It was a blocker we needed to workaround in cukespace for lambda support adding a "init" lifecycle for the lambdas.

The definition must be static. Otherwise steps can not be discovered without instantiating the test execution context (world, dependency injection context, ect). If this is confusing and does not seem like a solution, compare this to cucumber-java where all step definitions are static and no init hacks are needed.

To further clarify, this part of the DSL handles the registration of the step definition:

 .when("{int} gherkin(s) and {int} zukini(s)", ...

Then this part of DSL is executed only when the step is executed.

 (World world) -> (int gherkins, int zukinis) -> {
             world.setGherkins(gherkins);
             world.setZukinis(zukinis);
        }

Overly simplified a step definition would executed like so:

Function<World, Function<Integer, Integer>> stepDefinitionBody  =  (world) -> (gherkins, zukinis) -> {
             world.setGherkins(gherkins);
             world.setZukinis(zukinis);
}
stepDefinitionBody.apply(lookup.get(World.class)).apply(12, 12);

As you can see, unlike cucumber-java8 the world does only has to be instantiated to execute the step definition exactly because they are static.

@rmannibucau
Copy link
Contributor

The definition must be static. Otherwise steps can not be discovered without instantiating the test execution context (world, dependency injection context, ect). If this is confusing and does not seem like a solution, compare this to cucumber-java where all step definitions are static and no init hacks are needed.

This can be made compatible while the lambda gets access to the instance/context somehow but it fully defeats the concept to define them as field since it makes it all defined in static which requires the impl to do a lazy lookup.
I prefer the option to force the lambda def to be instantiated to be registered - this part is fine - but not injection aware at definition time and injection aware at step execution time only - thanks lambdas it works. Sounds like the least worse compromise for end users to me.

@mpkorstanje
Copy link
Contributor Author

I don't understand a word of what you are saying.

@rmannibucau
Copy link
Contributor

@mpkorstanje if you can't use injection it is generally pointless and prevents any evolution of cucumber setup (you are stucked to plain java). I think it is a bad thing so we must enable to use IoC since with suite complexity it eases things a lot.

Two options I see are:

  1. force a first param to be the world/context (nice but not fluent and close to the forced lookup solution)
  2. enable to use injections:
    2.a. public interface LambdaDef { void define(); } - https://github.com/cukespace/cukespace/blob/master/core/src/main/java/cucumber/runtime/arquillian/api/Lambda.java. You implement that to define a lambda, cucumber instantiate it and calls define to register the lambdas. At that stage injections are NOT available but lambdas can use it (resolved lazily by design).
    2.b. cucumber injects the LambdaDef impl enabling to use injections in the lambdas (https://github.com/cukespace/cukespace/blob/master/examples/src/test/java/cucumber/runtime/arquillian/java8/J8Test.java).

Indeed I prefer 2 which proved working well and stick to well known pattern rather than forcing users to write code through a new way and create helpers to solve this lookup+storage point.

@laeubi
Copy link

laeubi commented Apr 4, 2021

This is quite unlike the other Cucumber implementations where the shared context (world) is an explicit concept. And in other Cucumber implementations this shared context can be used even without dependency injection.

That's why I have suggested #1713 in the past. I don't see how lamdas can help here much. Even a super simple DI mechanism that only allows to inject other Glues via a setter like this

@Inject
public void setMyOtherGlue(GlueCode other) {

}

would be more profitable from my point of view than all these lambda stuff.

@rmannibucau
Copy link
Contributor

@laeubi DI is supported already with multiple IoC (from the plain jsr330 to the full 299 or more recent ones), just bring the underlying needed impl to get it. Lambda is really nice because it does not require to spread accross multiple method the state.

The very nice pattern I saw is something along:

final var state = new MyState();
Given(...., (xxx) -> state.reset(xxx));
When(..., (x) -> state.doX(x));
// ...

It enables to have properly scoped variables for steps known in the same "bucket" (like auth or things like that).
Mixed with IoC it is a very very fluent way to write steps.

@laeubi
Copy link

laeubi commented Apr 4, 2021

@rmannibucau of course its possible to maybe write your own Lamda Impl for cucumber as well... the point is, that at least one plain injection mechanism should be supported by cucumber-core so there is no need to add one extra.

For lamdas iteself, its fine for simple glues, but as they get more complex the lamdas get more and more confusing and as stated above its very hard to have good IDE support for them (as the IDE can hardly guess that a string is not a string but a parameter that forms a step).

@rmannibucau
Copy link
Contributor

@laeubi Not really, I'm not sure I get the point for cucumber-core to reimplement jsr330? there are tons of impls out there only supporting it (ie no more than jsr330) so I'm not sure it is needed - will not enable a single use case to end users is the point I want to highlight.
About your point about the IDE tooling, it is only harder when the matcher is not a constant string - in all other cases it is 1-1 with annotations and as hard to support than annotations.
These cases are rare and when they would be it just means the user will not get completeness of its steps/validation through the IDE which is not something highly used AFAIK so I think it is not a blocker anyway.
The big plus of lambdas is to enable decoration and composition by design (fn) whereas to do it on steps is sometimes hard - depends the IoC you use but with IoC not using lazy instantiation and supporting interception OOTB, ie the most common ones, it is very hard to do.

@laeubi
Copy link

laeubi commented Apr 4, 2021

I never wrote cucumber should "reimplement" anything. Pure cucumber-core does not support injection and/or "world" concept and that's the only reason why people are forced to use static fields to share state or use an additional DI framework.
The point is "we need lamdas to allow people share state" is clearly the wrong way round.

it is only harder when the matcher is not a constant string

Well... people don't really like IDEs that only works "sometimes" ...

@rmannibucau
Copy link
Contributor

@laeubi don't misinterpret what I wrote, I said I like the way you write tests as soon as you have lambdas in the context of a method. I never wrote "to share state" (and my example does not strictly do that even if it uses state keyword - likely abusively).
Anyway, lambda are the post-annotation way to work for java guys so I guess cucumber does not have the choice to provide a solution to it we like it or not. The solution should likely work by design and not rely on a static definition which prevents most of the usages making it interesting - was my point.

IDE point is right but from what I saw idea cucumber plugin works sometimes already so for lambdas it will not be worse anyway. And if you have usage stats I guess it will be low compared to downloads in all cases (can be neat to check though).

@laeubi
Copy link

laeubi commented Apr 4, 2021

And an example is just an example... There are valid cases where it makes sense to not put everything in one file and maybe share state e.g. for code reuse for example to write steps that combine other steps and people don't like to copy&paste all the code (what would be required with lamdas if no intermediate class is used).

Anyway, lambda are the post-annotation way to work for java guys

Never heard about that ... anyways lamdas and annotations are completely different so one can't exachange one for another. Annotations are compile time constant lamdas not, annotation have retention policies, lamdas not and so on.

And yes if your happy with simple text-matching then many ways works "not so worse" but there are people that like richer support :-) anyways I have no idea what downloadstat have to do with it...

I also don't want to hold anyone back invest time in this area, it was jsut asked for feedback here, if decision is already made its useless to ask ...

@mpkorstanje
Copy link
Contributor Author

mpkorstanje commented Apr 4, 2021

I feel the both of you fundamentally misunderstand a number of concepts involved.

An example without DI

Some domain objects.

public class GherkinPatch {

}

public class ZukiniPatch {

}

Then we can define all steps in the world:

public class World {

    private final GherkinPatch gherkinPatch = new GherkinPatch();
    private final ZukiniPatch zukiniPatch = new ZukiniPatch();

    @Given("A gherkin and a zukini")
    public void a_gherkin_and_zukini(){

    }
}

And also:

public class World implements En {

    private final GherkinPatch gherkinPatch = new GherkinPatch();
    private final ZukiniPatch zukiniPatch = new ZukiniPathc();

    public World(){ 
        Given("A gherkin and a zukini", () -> { 
        
        })
   }
}  

And also:

public class World {

    @Glue
    public static CucumberLambda glue = CucumberLambda
        .using(World.class)
        .step("A gherkin and a zukini", (World world) -> () -> {

        })

    private final GherkinPatch gherkinPatch = new GherkinPatch();
    private final ZukiniPatch zukiniPatch = new ZukiniPathc();

    public World(){ 
   }
}   

In all cases Cucumber must use the DefaultJavaObjectFactory to create an instance of World to provide the context for either the a_gherkin_and_zukini method, the () -> {} lambda or the (World world) -> () -> {} lambda to be executed against.

However unlike the cucumber-java8 step definitions the cucumber-java and cucumber-lambda step do not require that the world object is instantiated for the step definitions to be registered.

Also note that lambda step definitions need not be defined inside the World class. Even withouth DI the following is possible because the World class has a no-arg constructor:

public class LambdaStepDefinitions {

    @Glue
    public static CucumberLambda glue = CucumberLambda
        .using(World.class)
        .step("A gherkin and a zukini", (World world) -> () -> {

        });
}   

An example with dependency injection:

Again some domain objects:

public class GherkinPatch {  // instantiated by di, no dependencies

}

public class ZukiniPatch { // instantiated by di, no dependencies

}

And a world object, also created by DI.

public class World {

    private final GherkinPatch gherkinPatch;
    private final ZukiniPatch zukiniPatch;

    public World(GherkinPatch gherkinPatch, ZukiniPatch zukiniPatch){ // instantiated with arguments provided by DI
         this.gherkinPatch = gherkinPatch;
         this.zukiniPatch = zukiniPatch;    
   }

Now step definitions do not have to be defined in the World class to access it or its components:

public class AnnotationStepDefinitions1 {
   
    private final World world;
   
 public AnnotationStepDefinitions1(World world){
       ...
    }

    @Given("A gherkin and a zukini")
    public void a_gherkin_and_zukini(){

    }

}
public class AnnotationStepDefinitions2 {
   

    private final GherkinPatch gherkinPatch;
    private final ZukiniPatch zukiniPatch;
   
 public AnnotationStepDefinitions2(GherkinPatch gherkinPatch, ZukiniPatch zukiniPatch){
       ...
    }

    @Given("tend to the vegtable garden")
    public void a_gherkin_and_zukini(){

    }
}

Likewise:

public class Java8StepDefinitions 1implements En {

    public Java8StepDefinitions(World world){ 
        Given("A gherkin and a zukini", () -> { 
        
        });
   }
}  
public class Java8StepDefinitions2 implements En {

    public Java8StepDefinitions2(GherkinPatch gherkinPatch, ZukiniPatch zukiniPatch){ 
       Given("tend to the vegtable garden",() -> {

       });
   }
}  

And with cucumber lambda they can still be defined in the same file while using the world, or different components in the world.

public class LamdaStepDefinitions {
    @Glue
    public static CucumberLambda glue = CucumberLambda
        .using(World.class)
        .step("A gherkin and a zukini", (World world) -> () -> { // access to the world, provided by DI

        })
        .using(GherkinPatch.class, ZukiniPatch.class)
        .step("tend to the vegtable garden", (gherkinPatch, zukiniPatch) -> () -> {   // access to the vegetable patches, provided by DI

        });
}    

@rmannibucau
Copy link
Contributor

@laeubi yep and my proposal is compatible with all that, no copy paste required anyway.
@mpkorstanje yep but preventing lambda to use injections is very limiting. Java/lambda must be seen as language enabler, ioc as context enabler and both must be composable imo.

@mpkorstanje
Copy link
Contributor Author

I have shown you 3 equivalent examples that all use dependency injection

That you seem to think that one of these does not support injection leads me to believe that you misunderstood.

@rmannibucau
Copy link
Contributor

@mpkorstanje assuming it is proxied to handle the scope it works however I'm more interested in allowing field injection than constructor/lambda injection which makes the step writing quite fragile and subject to refactoring from what I experienced. Assuming it works all good. (but have to admit I have no idea how the static flavor would enable it, through an intermediate bean?)

@mattwynne
Copy link
Member

I like where you are going with this @mpkorstanje and it feels like it's worth exploring. I know it will feel alien to some people who've used Cucumber-JVM for a while and are used to the annotations, but I like the way it does away with the steps classes, and focusses on a World instead.

It's perhaps worth mentioning at this point an experiment @jbpros did recently with cucumber-js to actually eliminate mutable state completely: https://github.com/jbpros/cucumber-fp

@laeubi
Copy link

laeubi commented Apr 7, 2021

I have shown you 3 equivalent examples that all use dependency injection

Even though your example uses some kind of DI its mostly not that what most java-devleopers have in mind when they talk about DI...

As mentioned before I don't think forcing people to have any class that ultimately combines things might looks great on small examples but do not scale well.

To catch up with your examples:
Actually Gherkin and Zukini could be seen as generic Vegetables. Each of them might be developed by independent teams/companies and each of them providing a set of Stepdefs already.
Now it happens that they should be used in a Scenario that I plant them either in a garden or a glass-house (again both of them don't know of each other) and finally these might be used in some kind of "Farmer" steps...

So how would I

  1. get a reference to the glue defined in one of the lower libs
  2. call steps from there to combine smaller steps into higher level steps (reusing existing step definitions)

with any of the Lamda stuf presented here?

@mattwynne
Copy link
Member

@laeubi I'm trying to understand the purpose of your intervention here. Are you trying to give Rien feedback to help him to improve this proposal, or to try and persuade him that it's a bad idea and he should give up?

I don't think anyone is proposing that this replaces the current paradigm for defining steps using annotations, just that it becomes the new alternative. Am I understanding that correctly?

@aslakhellesoy
Copy link
Contributor

I suggest that everyone who feels strongly about how this should be done create a draft pull request and solicit early feedback. If there are multiple "competing" pull requests that's fine. It makes it easier to discuss in front of something concrete.

@mpkorstanje
Copy link
Contributor Author

With jhalterman/typetools#66 was released we can continue to use cucumber-java8 a little while longer.

@mpkorstanje
Copy link
Contributor Author

mpkorstanje commented Jun 1, 2021

And looks like cucumber-java8 also doesn't work on Correto when Kotlin and IDEAs build in compiler are used.

https://stackoverflow.com/questions/67787645/cucumber-kotlin-illegalstateexception-when-launching-test-via-intellij?r=SearchResults

@mpkorstanje
Copy link
Contributor Author

This looks like it could work.

But there is no way we can avoid using typetools. It does however make all step definitions static which allow us to discover them before running any tests.

package io.cucumber.java8;

import io.cucumber.java8.ApiSketch.StepDefinitionFunctionSupplier.C1A2;
import io.cucumber.java8.ApiSketch.StepDefinitionFunctionSupplier.StepDefinitionBody;

public class ApiSketch {

    public static class World {
        public void setGherkins(int i) {
        }

        public void setZukinis(int i) {
        }

    }

    @Glue
    public static StepDefinitions<World> stepDefinitions = CucumberLambda
            .using(World.class)
            .beforeAll(() -> {

            })
            .step("A gherkin and a zukini", (World world) -> () -> {
                world.setGherkins(1);
                world.setZukinis(1);
            })
            .step("A gherkin", (World world) -> (Integer gerkin) -> {

            })
            .step("{int} gherkin(s) and {int} zukini(s)", (World world) -> (Integer gherkins, Integer zukinis) -> {
                world.setGherkins(gherkins);
                world.setZukinis(zukinis);
            });

    public @interface Glue {
    }

    public static final class CucumberLambda {

        static <T> StepDefinitions<T> using(Class<T> context) {
            return new StepDefinitions<>(context);
        }

    }

    private static class StepDefinitions<Context> {

        public StepDefinitions(Class<Context> context) {

        }

        public StepDefinitions<Context> step(String expression, StepDefinitionFunctionSupplier.C1A0<Context> body) {
            return this;
        }

        public <A1> StepDefinitions<Context> step(String expression,
                StepDefinitionFunctionSupplier.C1A1<Context, A1> body) {
            return this;
        }

        public <A1, A2> StepDefinitions<Context> step(String expression, C1A2<Context, A1, A2> body) {
            return this;
        }

        public StepDefinitions<Context> beforeAll(StepDefinitionBody.A0 body) {
            return this;
        }

        public StepDefinitions<World> parameterType(String amount, String pattern, Object o) {
            return null;
        }

    }

    public interface StepDefinitionFunctionSupplier {

        @FunctionalInterface
        interface C0A0 extends StepDefinitionFunctionSupplier {
            StepDefinitionBody.A0 accept();

        }

        @FunctionalInterface
        interface C1A0<C1> extends StepDefinitionFunctionSupplier {
            StepDefinitionBody.A0 accept(C1 c1);

        }

        @FunctionalInterface
        interface C1A1<C1, A1> extends StepDefinitionFunctionSupplier {
            StepDefinitionBody.A1<A1> accept(C1 c1);

        }

        @FunctionalInterface
        interface C1A2<C1, A1, A2> extends StepDefinitionFunctionSupplier {
            StepDefinitionBody.A2<A1, A2> accept(C1 c1);

        }

        interface StepDefinitionBody {

            @FunctionalInterface
            interface A0 extends StepDefinitionBody {
                void accept() throws Throwable;

            }

            @FunctionalInterface
            interface A1<T1> extends StepDefinitionBody {
                void accept(T1 p1) throws Throwable;

            }

            @FunctionalInterface
            interface A2<T1, T2> extends StepDefinitionBody {
                void accept(T1 p1, T2 p2) throws Throwable;

            }

        }

    }

}

@mpkorstanje
Copy link
Contributor Author

Note to self, consider improving the error message about dependency injection.

@mpkorstanje
Copy link
Contributor Author

Note to self, consider cucumber-java-lambda to stay consistent with -scala and potentially -kotlin.

@mpkorstanje
Copy link
Contributor Author

mpkorstanje commented Apr 25, 2022

Some more sketches in https://github.com/mpkorstanje/cucumber-lambda-proposals

There are 4 options to consider:

package io.cucumber;

import io.cucumber.lambda.Glue;
import io.cucumber.lambda.StepDefinitions;
import io.cucumber.lambda.context.GherkinPatch;
import io.cucumber.lambda.context.World;
import io.cucumber.lambda.context.ZukiniPatch;

import static io.cucumber.lambda.StepDefinitions.using;
import static io.cucumber.lambda.StepDefinitions.with;

@SuppressWarnings("unused")
public class ApiSketch {

    /**
     * Advantages:
     * 1. Clear visual separation between context and step definition.
     * 2. Lambdas provide natural formatting breaks
     * 3. Allows method extraction.
     * 4. Kotlin equivalent can use "Function literals with receiver"
     * Disadvantages:
     * 1. Visually a bit verbose
     */
    @Glue
    public static StepDefinitions doubleLambda = using(World.class)
            .step("{int} gherkin(s) and {int} zukini(s)", (World world) -> (Integer gherkins, Integer zukinis) -> {
                world.setGherkins(gherkins);
                world.setZukinis(zukinis);
            });

    @Glue
    public static StepDefinitions doubleLambdaWithMethodReference = using(World.class)
            .step("{int} gherkin(s) and {int} zukini(s)", (World world) -> world::setGherkinsAndZukinis);

    /**
     * Advantages:
     * 1. Delays the need for dependency injection
     * 2. Would be different from Kotlin equivalent
     * Disadvantages:
     * 1. Visually a very verbose
     */
    @Glue
    public static StepDefinitions doubleLambdaWithMultiContexts = using(GherkinPatch.class, ZukiniPatch.class)
            .step("{int} gherkin(s) and {int} zukini(s)",
                    (GherkinPatch gherkinPatch, ZukiniPatch zukiniPatch) -> (Integer gherkins, Integer zukinis) -> {
                        gherkinPatch.setGherkins(gherkins);
                        zukiniPatch.setZukinis(zukinis);
                    });

    /**
     * Advantages:
     * 1. Visually short
     * Disadvantages:
     * 1. No separation between context and step definition function
     * 2. No method extraction
     */
    @Glue
    public static StepDefinitions singleLambda = with(World.class)
            .step("{int} gherkin(s) and {int} zukini(s)", (World world, Integer gherkins, Integer zukinis) -> {
                world.setGherkins(gherkins);
                world.setZukinis(zukinis);
            });

    @Glue
    public static StepDefinitions singleLambdaWithMultipleContext = with(GherkinPatch.class, ZukiniPatch.class)
            .step("{int} gherkin(s) and {int} zukini(s)",
                    (GherkinPatch gherkinPatch, ZukiniPatch zukiniPatch, Integer gherkins, Integer zukinis) -> {
                        gherkinPatch.setGherkins(gherkins);
                        zukiniPatch.setZukinis(zukinis);
                    });

}

@mpkorstanje mpkorstanje changed the title Consider cucumber-lambda as a replacement for cucumber-java8 Consider cucumber-java-lambda as a replacement for cucumber-java8 Apr 27, 2022
@mpkorstanje mpkorstanje linked a pull request Apr 29, 2022 that will close this issue
7 tasks
mpkorstanje added a commit to cucumber/docs that referenced this issue Aug 11, 2022
Cucumber Java8 is considered for deprecation[1]. Recommending it in the docs
suggests otherwise.

1: cucumber/cucumber-jvm#2279

Fixes: #767
@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
@mpkorstanje mpkorstanje removed the ⌛ stale Will soon be closed by stalebot unless there is activity label Apr 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
⚡ enhancement Request for new functionality
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants