JBehave
  1. JBehave
  2. JBEHAVE-702

Allow directives to support multiple step pattern variant

    Details

    • Type: New Feature New Feature
    • Status: Resolved Resolved
    • Priority: Major Major
    • Resolution: Fixed
    • Affects Version/s: 3.5.4
    • Fix Version/s: 3.6
    • Component/s: Core
    • Labels:
    • Number of attachments :
      3

      Description

      To get more natural German story texts, it would be very helpful if the Steps.listCandidates() method could be amended.
      This would allow for automatically generating slight variations of the texts without having to specify them all as aliases. Currently I am doing this:

      @Then("soll foo bar with $xyz")
      @Aliases(values =

      { "foo bar with $xyz", "soll foo bar: $xyz", "foo bar: $xyz" }

      )

      The word "soll" is made optional this way. The reasoning behind this is that when using "And" it is more natural to write

      Then soll something else
      And foo bar with $xyz

      instead of

      Then soll something else
      And soll foo bar with $xyz

      It would be very nice to not have define aliases for all these variants, because it makes the code harder to maintain. Instead, I though I'd add this dynamically, the same way annotated Aliases are, but it seems this functionality would require providing a new subclass of org.jbehave.core.steps.Steps which I cannot see how to do.

      I suggest a change like this:

      1) In org.jbehave.core.steps.Steps add another call in listCandidates() after each of the addCandidatesFromAliases(...) like addCustomDerivedCandidates(...) with the same signature and a protected empty default implementation.
      2) Allow configuration to specify a custom subclass of org.jbehave.core.steps.Steps. In there custom additions to the candidates list could be made, like the one I described.

        Activity

        Hide
        Mauro Talevi added a comment -

        Hi Daniel, not very clear how your "dynamical" approach would work and how generic this solution would be. Can you provide an example of your proposed addCustomDerivedCandidates().

        In general, I think subclassing steps is not a good solution as it mixes declaration and implementation.

        If there is a need, we can enhance the declarative functionality, either via modifying the @Aliases annotation or providing a new one.

        Show
        Mauro Talevi added a comment - Hi Daniel, not very clear how your "dynamical" approach would work and how generic this solution would be. Can you provide an example of your proposed addCustomDerivedCandidates(). In general, I think subclassing steps is not a good solution as it mixes declaration and implementation. If there is a need, we can enhance the declarative functionality, either via modifying the @Aliases annotation or providing a new one.
        Hide
        Daniel Schneller added a comment -

        Reading your comment I realized that my original idea was maybe a little too complicated for what I had in mind. I tried an implementation locally by introducing "@Variant"/@Variants with the same structure as "@Alias"/"@Aliases".

        Doing this:

        @Then("foo bar with $xyz")
        @Alias("foo bar: $xyz"
        @Variant("soll")
        public void foobar()

        {...}

        would lead to these 4 permutations all being added as candidates:

        foo bar with $xyz
        soll foo bar with $xyz
        foo bar: $xyz
        soll foo far: $xyz

        In Steps.java I added this method:

        private void addVariants(List<StepCandidate> candidates, Method method,
        StepType stepType, String alias, int priority) {
        List<String> allVariants = getVariants(method);
        for (String variant : allVariants)

        { addCandidate(candidates, method, stepType, variant + " " + alias, priority); }

        }

        which is called from each of the three blocks in listCandidates() and the two blocks in addCandidatesFromAliases(...).

        That way the change is very minimal and remains fully backwards compatible.

        I will attach my modified Steps class.

        Show
        Daniel Schneller added a comment - Reading your comment I realized that my original idea was maybe a little too complicated for what I had in mind. I tried an implementation locally by introducing "@Variant"/@Variants with the same structure as "@Alias"/"@Aliases". Doing this: @Then("foo bar with $xyz") @Alias("foo bar: $xyz" @Variant("soll") public void foobar() {...} would lead to these 4 permutations all being added as candidates: foo bar with $xyz soll foo bar with $xyz foo bar: $xyz soll foo far: $xyz In Steps.java I added this method: private void addVariants(List<StepCandidate> candidates, Method method, StepType stepType, String alias, int priority) { List<String> allVariants = getVariants(method); for (String variant : allVariants) { addCandidate(candidates, method, stepType, variant + " " + alias, priority); } } which is called from each of the three blocks in listCandidates() and the two blocks in addCandidatesFromAliases(...). That way the change is very minimal and remains fully backwards compatible. I will attach my modified Steps class.
        Hide
        Daniel Schneller added a comment -

        Modified Steps.java with additions for @Variant(s).

        Show
        Daniel Schneller added a comment - Modified Steps.java with additions for @Variant(s).
        Hide
        Mauro Talevi added a comment -

        Why not simply add an optional "variants" property to the @Alias/es annotations?

        Show
        Mauro Talevi added a comment - Why not simply add an optional "variants" property to the @Alias/es annotations?
        Hide
        Daniel Schneller added a comment -

        Good point. However, as it would be a reasonable requirement to have variants without other "classic" aliases, I needed to make the value/s attribute of the Alias/Aliases annotations optional, too. Attached find a new version of the Steps class that implements this change. If you like, I could also upload Alias/Aliases, but as the change in there is minimal, I did not bother.

        Show
        Daniel Schneller added a comment - Good point. However, as it would be a reasonable requirement to have variants without other "classic" aliases, I needed to make the value/s attribute of the Alias/Aliases annotations optional, too. Attached find a new version of the Steps class that implements this change. If you like, I could also upload Alias/Aliases, but as the change in there is minimal, I did not bother.
        Hide
        Mauro Talevi added a comment -

        I'm not sure there is a real difference between a variant and an alias. The effect is in any case to create another step candidate with a modified pattern.

        Do you have a use case where you'd use @Variant without @Alias?

        Show
        Mauro Talevi added a comment - I'm not sure there is a real difference between a variant and an alias. The effect is in any case to create another step candidate with a modified pattern. Do you have a use case where you'd use @Variant without @Alias?
        Hide
        Daniel Schneller added a comment - - edited

        Here a few examples where I would not like to create an alias for given and then cases, because they would be virtually identical, but for the "variant" word.

        A little German lesson upfront

        • Gegeben = Given
        • Dann = then
        • sei = be
        • soll = should

        The "sei" (be) is associated with the first "Gegeben" in a German sentence, further conditions added with "and" do not repeat it. Doing so would be very awkward language.

        The same goes for "soll" (should). The first part of a "Then" very often gets it, the following ones do not.

        The difference to, say, English is a) the repetition and b) the order of the words. While in English you'd write

        ... then A should be 1 and B should be 2 ...

        in German it would be

        ... dann sollte A 1 sein und B 2 sein ...

        So, I agree to your idea that a new attribute of the existing Alias/es would be enough, because that's what it really is. What I am trying to avoid is having to multiply aliases to build all permutations with these variant words in front of them (there are a few more than "sei" and "soll", e. g. "sollte" as an equivalent to "soll" in most cases).

        Having a way to add these concisely would increase legibility of the code and also help avoid copy/paste errors.

        A few examples:

        --------------------
        Good:
        Gegeben sei eine Usersession ["sei" for the first case]
        Und ein Artikel im Warenkorb [no repeated "sei"]

        Also good, equivalent:
        Gegeben sei ein Artikel im Warenkorb ["sei" for the first case]
        Und eine Usersession [no repeated "sei"]

        Not Good:
        Gegeben sei eine Usersession
        Und sei ein Artikel im Warenkorb [repeated "sei" very awkward]
        --------------------

        Could be written as

        @Given("eine Usersession")
        @Aliases(values=

        {"eine bestehende Session","eine aktive Session"}

        ,variants=

        {"sei"})

        @Given("ein Artikel im Warenkorb")
        @Alias(variants={"sei"}

        )

        --------------------
        Good:
        Dann soll eine Meldung angezeigt werden ["soll" for first case]
        Und ein Logeintrag geschrieben werden [not repeated]

        Not Good:
        Dann soll und ein Logeintrag geschrieben werden
        Und soll eine Meldung angezeigt werden [repetition awkward]
        --------------------

        Could be written as

        @Then("eine Meldung angezeigt werden")
        @Aliases(variants=

        {"soll","sollte"})

        @Then("eine Meldung angezeigt werden")
        @Aliases(variants={"soll","sollte"}

        )

        Without the variants, I would either need to write down a lot of aliases explicitly, or the people writing the tests would not be at their leisure to modify the order of the conditions they use without sacrificing natural language rules.

        I hope I get the point across. It is a little difficult to describe the peculiarities of one language with the words of the other

        Show
        Daniel Schneller added a comment - - edited Here a few examples where I would not like to create an alias for given and then cases, because they would be virtually identical, but for the "variant" word. A little German lesson upfront Gegeben = Given Dann = then sei = be soll = should The "sei" (be) is associated with the first "Gegeben" in a German sentence, further conditions added with "and" do not repeat it. Doing so would be very awkward language. The same goes for "soll" (should). The first part of a "Then" very often gets it, the following ones do not. The difference to, say, English is a) the repetition and b) the order of the words. While in English you'd write ... then A should be 1 and B should be 2 ... in German it would be ... dann sollte A 1 sein und B 2 sein ... So, I agree to your idea that a new attribute of the existing Alias/es would be enough, because that's what it really is. What I am trying to avoid is having to multiply aliases to build all permutations with these variant words in front of them (there are a few more than "sei" and "soll", e. g. "sollte" as an equivalent to "soll" in most cases). Having a way to add these concisely would increase legibility of the code and also help avoid copy/paste errors. A few examples: -------------------- Good: Gegeben sei eine Usersession ["sei" for the first case] Und ein Artikel im Warenkorb [no repeated "sei"] Also good, equivalent: Gegeben sei ein Artikel im Warenkorb ["sei" for the first case] Und eine Usersession [no repeated "sei"] Not Good: Gegeben sei eine Usersession Und sei ein Artikel im Warenkorb [repeated "sei" very awkward] -------------------- Could be written as @Given("eine Usersession") @Aliases(values= {"eine bestehende Session","eine aktive Session"} ,variants= {"sei"}) @Given("ein Artikel im Warenkorb") @Alias(variants={"sei"} ) -------------------- Good: Dann soll eine Meldung angezeigt werden ["soll" for first case] Und ein Logeintrag geschrieben werden [not repeated] Not Good: Dann soll und ein Logeintrag geschrieben werden Und soll eine Meldung angezeigt werden [repetition awkward] -------------------- Could be written as @Then("eine Meldung angezeigt werden") @Aliases(variants= {"soll","sollte"}) @Then("eine Meldung angezeigt werden") @Aliases(variants={"soll","sollte"} ) Without the variants, I would either need to write down a lot of aliases explicitly, or the people writing the tests would not be at their leisure to modify the order of the conditions they use without sacrificing natural language rules. I hope I get the point across. It is a little difficult to describe the peculiarities of one language with the words of the other
        Hide
        Alexander Lehmann added a comment -

        I wonder if it would be feasible to write an alternate steps matcher that supports optional words or variants in the step sentence (similar to a regular expression pattern), I think this would make writing alternates easier.

        Dann soll eine Meldung angezeigt werden
        Und (soll )?ein Logeintrag geschrieben werden

        Show
        Alexander Lehmann added a comment - I wonder if it would be feasible to write an alternate steps matcher that supports optional words or variants in the step sentence (similar to a regular expression pattern), I think this would make writing alternates easier. Dann soll eine Meldung angezeigt werden Und (soll )?ein Logeintrag geschrieben werden
        Hide
        Mauro Talevi added a comment -

        I've been coming to a similar conclusion - that is to provide a pattern language to express variants of aliases. Only, I would keep it separate from the step pattern parser, because else it would complicate things considerably (between what is the pattern language and its underlying regex expressions). I would have it instead in the Steps class when the candidate steps are created for each new step pattern.

        We should collect the desired behaviour with examples expressed in English so as to gather the maximum feedback. I'm pretty sure this sort of requirement, while applicable in different ways in different languages, is actually quite common.

        Show
        Mauro Talevi added a comment - I've been coming to a similar conclusion - that is to provide a pattern language to express variants of aliases. Only, I would keep it separate from the step pattern parser, because else it would complicate things considerably (between what is the pattern language and its underlying regex expressions). I would have it instead in the Steps class when the candidate steps are created for each new step pattern. We should collect the desired behaviour with examples expressed in English so as to gather the maximum feedback. I'm pretty sure this sort of requirement, while applicable in different ways in different languages, is actually quite common.
        Hide
        Daniel Schneller added a comment -

        Alright, I have come up with something that would work for me. Feedback appreciated
        It is based on the premise that the usual case would be to have some parts of a step be optional or picked from a list of variants like so.

        Example 1: two variants

        @When("$A plus $B")
        @Alias("$A + $B")
        

        could become:

        @When("@$A {plus|+}")
        

        Example 2: optional phrases with alternatives

        @Then("the result be $x")
        @Alias(values={"the result must be $x",
                       "the result should be $x",
                       "the result has to be $x"})
        

        could be written as

        @Then("the result {must |should |has to |}be")
        

        Here the trailing | introduces an "empty" variant, essentially making the pattern optional.

        Example 3: multiple patterns in one step
        It is possible to have more than one pattern group in a step description.

        @Then("A {must |has to |is to |} be $x unless {it's|it is} {part of|contained in} {list |}$y")
        

        In this example writing this with aliases would result in 31 aliased variants (4*2*2*2 permutations).

        I will attach a small ZIP file containing a modified Steps class and the permutation builder I have hacked together. As said before, any feedback is welcome.

        Show
        Daniel Schneller added a comment - Alright, I have come up with something that would work for me. Feedback appreciated It is based on the premise that the usual case would be to have some parts of a step be optional or picked from a list of variants like so. Example 1: two variants @When( "$A plus $B" ) @Alias( "$A + $B" ) could become: @When("@$A {plus|+}") Example 2: optional phrases with alternatives @Then( "the result be $x" ) @Alias(values={ "the result must be $x" , "the result should be $x" , "the result has to be $x" }) could be written as @Then( "the result {must |should |has to |}be" ) Here the trailing | introduces an "empty" variant, essentially making the pattern optional. Example 3: multiple patterns in one step It is possible to have more than one pattern group in a step description. @Then( "A {must |has to |is to |} be $x unless {it's|it is} {part of|contained in} {list |}$y" ) In this example writing this with aliases would result in 31 aliased variants (4*2*2*2 permutations). I will attach a small ZIP file containing a modified Steps class and the permutation builder I have hacked together. As said before, any feedback is welcome.
        Hide
        Mauro Talevi added a comment -

        I like the general approach, but would perhaps provide the patterns with alternatives via the @Alias annotation, rather than the @Given/When/Then. Essentially, we'd be enhancing the language of aliases, with each alias (or permutation thereof) translating into a separate step candidate.

        Show
        Mauro Talevi added a comment - I like the general approach, but would perhaps provide the patterns with alternatives via the @Alias annotation, rather than the @Given/When/Then. Essentially, we'd be enhancing the language of aliases, with each alias (or permutation thereof) translating into a separate step candidate.
        Hide
        Daniel Schneller added a comment -

        My modified Steps class in the ZIP works on both. Of course, it could be limited to calling the permutation generator for aliases only.
        However, what would you put into the "canonical" (i. e. @Given, @When, @Then) annotations in that case? Maybe the first option of each variant pattern?

        From what I can see, that would bring back the redundancy I was trying to eliminate in the first place, especially with multiple patterns in one line.

        @Then("A must be $x unless it's part of $y")
        @Alias("A {must|has to|is to}be $x unless {it's|it is} {part of|contained in} {list |}$y")
        

        However, even when only adding the new feature to aliases, I see a problem with backwards compatibility in cases where existing stories contain lines that for some reason already have curly braces and/or pipes in them. Those would break if the expansion were to suddenly take place, not matter if it were in the aliases or the primary annotations.

        For that reason I'd say one could add a (default false) boolean attribute to the @Given/@When/@Then annotations that would require a conscious and explicit decision to enable pattern expansion.

        Like this:
        Example 1: unchanged behavior for existing story lines

        @Then("A {must |has to |is to |} be $x unless {it's|it is} {part of|contained in} {list |}$y")
        

        Would not be expanded, but used verbatim, just like it is today.

        Example 2: explicitly enabled expansion

        @Then(value="A {must |has to |is to |} be $x unless {it's|it is} {part of|contained in} {list |}$y",
              permutate=true)
        

        Would work as described above.

        The same would go for Alias/es as well to maximize backwards compatibility.
        Maybe, depending on feature adoption, one could argue to change the default to be true in some distant future version, however I think it would would not really be necessary.

        Show
        Daniel Schneller added a comment - My modified Steps class in the ZIP works on both. Of course, it could be limited to calling the permutation generator for aliases only. However, what would you put into the "canonical" (i. e. @Given, @When, @Then) annotations in that case? Maybe the first option of each variant pattern? From what I can see, that would bring back the redundancy I was trying to eliminate in the first place, especially with multiple patterns in one line. @Then( "A must be $x unless it's part of $y" ) @Alias( "A {must|has to|is to}be $x unless {it's|it is} {part of|contained in} {list |}$y" ) However, even when only adding the new feature to aliases, I see a problem with backwards compatibility in cases where existing stories contain lines that for some reason already have curly braces and/or pipes in them. Those would break if the expansion were to suddenly take place, not matter if it were in the aliases or the primary annotations. For that reason I'd say one could add a (default false) boolean attribute to the @Given/@When/@Then annotations that would require a conscious and explicit decision to enable pattern expansion. Like this: Example 1: unchanged behavior for existing story lines @Then( "A {must |has to |is to |} be $x unless {it's|it is} {part of|contained in} {list |}$y" ) Would not be expanded, but used verbatim, just like it is today. Example 2: explicitly enabled expansion @Then(value= "A {must |has to |is to |} be $x unless {it's|it is} {part of|contained in} {list |}$y" , permutate= true ) Would work as described above. The same would go for Alias/es as well to maximize backwards compatibility. Maybe, depending on feature adoption, one could argue to change the default to be true in some distant future version, however I think it would would not really be necessary.
        Hide
        Brian Repko added a comment -

        Been sort of watching this issue go by - thought I'd jump in. At first I thought that perhaps this points to variant keyword expression but now see that that isn't the case.

        If we go the route of permutable @Given/@When/@Then, then we could introduce a regex escape character - allowing the author to specify the explicit regex for that part of the value. In Daniel's examples, the '

        {' and '}

        ' characters do that. But it could look a bit like JAX-RS @Path regex escapes as well - if you are looking for a syntax that is already in use in the java world. JAX-RS does this around variables but perhaps we could introduce the variable as optional.

        @Then(value="A {:must|has to|is to} be

        {x}

        unless {:it's|it is} {:part of|contained in} {:list?}

        {y}

        ")
        @Then(value="A $:must|has to|is to be $x unless $:it's|it is $:part of|contained in $:list?$y")

        Problem as you can tell is that we have variable prefix but not suffix (variable ends at whitespace).
        It may be that if you want to use regex, then you have to have prefix AND suffix.

        Just my thought from another space (JAX-RS) that is somewhat similar.

        Also be careful with your spaces - matching on your examples requires 2 spaces - "A must be $x unless it's..."

        Show
        Brian Repko added a comment - Been sort of watching this issue go by - thought I'd jump in. At first I thought that perhaps this points to variant keyword expression but now see that that isn't the case. If we go the route of permutable @Given/@When/@Then, then we could introduce a regex escape character - allowing the author to specify the explicit regex for that part of the value. In Daniel's examples, the ' {' and '} ' characters do that. But it could look a bit like JAX-RS @Path regex escapes as well - if you are looking for a syntax that is already in use in the java world. JAX-RS does this around variables but perhaps we could introduce the variable as optional. @Then(value="A {:must|has to|is to} be {x} unless {:it's|it is} {:part of|contained in} {:list?} {y} ") @Then(value="A $:must|has to|is to be $x unless $:it's|it is $:part of|contained in $:list?$y") Problem as you can tell is that we have variable prefix but not suffix (variable ends at whitespace). It may be that if you want to use regex, then you have to have prefix AND suffix. Just my thought from another space (JAX-RS) that is somewhat similar. Also be careful with your spaces - matching on your examples requires 2 spaces - "A must be $x unless it's..."
        Hide
        Mauro Talevi added a comment -

        Hi Daniel, nice work. I've applied and pushed patch (slightly modified wrt names).

        Show
        Mauro Talevi added a comment - Hi Daniel, nice work. I've applied and pushed patch (slightly modified wrt names).
        Hide
        Mauro Talevi added a comment -

        Hi Daniel, can you confirm if this issue can be reseolved? Does the current master satisfy your usecase?

        Show
        Mauro Talevi added a comment - Hi Daniel, can you confirm if this issue can be reseolved? Does the current master satisfy your usecase?
        Hide
        Ivan Verdezoto added a comment -

        Hi,
        I updated to jbehave-core 3.6 and I am not able to use the pattern variant.
        All steps had matched perfect before with this configuration,
        But after I changed some steps (in the steps class) with the variant notation, Jbehave marks thess steps as pending. Do I miss something? Should I change something in the configuration class?

        Here is a dumb tested example of what I got:

        • Story file:

        Scenario: Test

        Given A x B 10.0
        And A y B 1.0

        • Steps class:

        @Given("A

        {x|y}

        B $quantity")
        public void theItemPriceIs(String q)

        { // ... }
        • Configuration:

        MostUsefulConfiguration()
        .useStoryControls(new StoryControls().doSkipScenariosAfterFailure(false))
        .useStoryLoader(new LoadFromClasspath(embeddableClass.getClassLoader()))
        .usePathCalculator(new RelativePathCalculator())
        .useStoryReporterBuilder(reporterBuilder);

        • Eclipse console output:

        Processing system properties {}
        Using controls EmbedderControls[batch=false,skip=false,generateViewAfterStories=true,ignoreFailureInStories=true,ignoreFailureInView=true,verboseFailures=false,verboseFiltering=false,storyTimeoutInSecs=300,threads=1]
        Using 1 threads

        (BeforeStories)

        Running story jbehave/tmp_stories/123.story

        (jbehave/tmp_stories/123.story)
        Scenario: Test
        Given A x B 10.0 (PENDING)
        And A y B 1.0 (PENDING)
        @Given("A x B 10.0")
        @Pending
        public void givenAXB100()

        { // PENDING }

        @Given("A y B 1.0")
        @Pending
        public void givenAYB10(){ // PENDING }

        .
        .
        .

        Show
        Ivan Verdezoto added a comment - Hi, I updated to jbehave-core 3.6 and I am not able to use the pattern variant. All steps had matched perfect before with this configuration, But after I changed some steps (in the steps class) with the variant notation, Jbehave marks thess steps as pending. Do I miss something? Should I change something in the configuration class? Here is a dumb tested example of what I got: Story file: Scenario: Test Given A x B 10.0 And A y B 1.0 Steps class: @Given("A {x|y} B $quantity") public void theItemPriceIs(String q) { // ... } Configuration: MostUsefulConfiguration() .useStoryControls(new StoryControls().doSkipScenariosAfterFailure(false)) .useStoryLoader(new LoadFromClasspath(embeddableClass.getClassLoader())) .usePathCalculator(new RelativePathCalculator()) .useStoryReporterBuilder(reporterBuilder); Eclipse console output: Processing system properties {} Using controls EmbedderControls [batch=false,skip=false,generateViewAfterStories=true,ignoreFailureInStories=true,ignoreFailureInView=true,verboseFailures=false,verboseFiltering=false,storyTimeoutInSecs=300,threads=1] Using 1 threads (BeforeStories) Running story jbehave/tmp_stories/123.story (jbehave/tmp_stories/123.story) Scenario: Test Given A x B 10.0 (PENDING) And A y B 1.0 (PENDING) @Given("A x B 10.0") @Pending public void givenAXB100() { // PENDING } @Given("A y B 1.0") @Pending public void givenAYB10(){ // PENDING } . . .
        Hide
        Daniel Schneller added a comment -

        Should not be anything special you need to do. I tried your method with the annotations (copy and pasted them), and it worked ok. I suspect you might not have switched to 3.6 after all. Maybe double check (if need be with a debugger), which JAR is being loaded at runtime. However, I think further bug hunting is not for JIRA, but would better happen on a mailing list? (http://jbehave.org/mailing-lists.html)

        Show
        Daniel Schneller added a comment - Should not be anything special you need to do. I tried your method with the annotations (copy and pasted them), and it worked ok. I suspect you might not have switched to 3.6 after all. Maybe double check (if need be with a debugger), which JAR is being loaded at runtime. However, I think further bug hunting is not for JIRA, but would better happen on a mailing list? ( http://jbehave.org/mailing-lists.html )

          People

          • Assignee:
            Mauro Talevi
            Reporter:
            Daniel Schneller
          • Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

            Dates

            • Created:
              Updated:
              Resolved: