-
-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Comments
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 👎 |
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? |
Looking at the monthly stats I think there is already sufficient interest in lambda's:
-- 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 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 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.
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 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 |
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 don't understand a word of what you are saying. |
@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:
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. |
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
would be more profitable from my point of view than all these lambda stuff. |
@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:
It enables to have properly scoped variables for steps known in the same "bucket" (like auth or things like that). |
@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). |
@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. |
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.
Well... people don't really like IDEs that only works "sometimes" ... |
@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). 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). |
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).
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 ... |
I feel the both of you fundamentally misunderstand a number of concepts involved. An example without DISome 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 However unlike the Also note that lambda step definitions need not be defined inside the 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 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
});
} |
@laeubi yep and my proposal is compatible with all that, no copy paste required anyway. |
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. |
@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?) |
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 |
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: So how would I
with any of the Lamda stuf presented here? |
@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? |
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. |
With jhalterman/typetools#66 was released we can continue to use |
And looks like |
This looks like it could work. But there is no way we can avoid using 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;
}
}
}
} |
Note to self, consider improving the error message about dependency injection. |
Note to self, consider |
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);
});
} |
Cucumber Java8 is considered for deprecation[1]. Recommending it in the docs suggests otherwise. 1: cucumber/cucumber-jvm#2279 Fixes: #767
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. |
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 withcucumber-java
. So there is a good reason to use lambda's to define step definitions.Compare:
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).Additionally Cucumber uses
typetools
to determine the type of lambda parameters. This requires the use of ofUnsafe
to fish in the constant pool. This is a non-trivial process and Cucumber currently usestypetools
to facilitate this. However because this fundamentally depends on unsafe operation it is not guaranteed to work in the long run.Requested solution
cucumber-lambda
as an alternative forcucumber-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 waycuucmber-java
discovers step definitions and avoids the issues ofcucumber-java8
.Avoid the use of
typetools
where possible by specifying all parameter typesThe
World
object is created using DI as usual. Consider the possibility of defining steps/hooks using multiple objects.Out of scope
The text was updated successfully, but these errors were encountered: