cucumber / cucumber-jvm

Cucumber for the JVM
https://cucumber.io
MIT License
2.7k stars 2.02k forks source link

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

Open mpkorstanje opened 3 years ago

mpkorstanje commented 3 years ago

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.
laeubi commented 3 years ago

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 :-1:

aslakhellesoy commented 3 years ago

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 commented 3 years ago

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) -> () -> {

        });
}
rmannibucau commented 3 years ago

@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 commented 3 years ago

@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 commented 3 years ago

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 commented 3 years ago

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

rmannibucau commented 3 years ago

@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 commented 3 years ago

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 commented 3 years ago

@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 commented 3 years ago

@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 commented 3 years ago

@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 commented 3 years ago

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 commented 3 years ago

@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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

@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 commented 3 years ago

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 commented 3 years ago

@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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

@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 commented 3 years ago

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 commented 3 years ago

With https://github.com/jhalterman/typetools/pull/66 was released we can continue to use cucumber-java8 a little while longer.

mpkorstanje commented 3 years ago

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 commented 2 years ago

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 commented 2 years ago

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

mpkorstanje commented 2 years ago

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

mpkorstanje commented 2 years ago

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);
                    });

}
stale[bot] commented 1 year ago

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.