Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Support for Calling Steps Explicitly #634

Closed
machineghost opened this issue Sep 9, 2016 · 8 comments
Closed

Support for Calling Steps Explicitly #634

machineghost opened this issue Sep 9, 2016 · 8 comments

Comments

@machineghost
Copy link

machineghost commented Sep 9, 2016

As this Stack Overflow answer explains, in (Ruby) Cucumber it is possible to call one step from another:

Given /^(.*) is logged in$/ do |name|
step "I log in as #{name}"
end

It seems like this could very easily be implemented in the JS version:

this.Given(/^(.*) is logged in/$, function(name) {
    this.callStep(`I log in as ${name}`)
}

Now I (think I) know what you're going to say: just factor out the logic in to a shared function:

this.Given(/^(.*) is logged in/$, function(name) {
    loginAs(name)
}

But that approach has a couple downsides:

A) It's more verbose; consider this very simple example of a single repetition:

this.Then('whatever', function() {
    doSomething();
})
this.Then('something', function() {
    this.callStep('whatever');
}

vs.

function doSomething() {
    this.callStep('whatever');
}
this.Then('whatever', function() {
    doSomething();
})
this.Then('something', function() {
    doSomething();
}

... and that's just when you do it once (the more you repeat the more bloat you get), without passing anything around (obviously a real world example would be even more bloated as at the very least you have to pass this all over the place).

B) It winds up decoupling all of your logic from your steps. Let's say you want to repeat the steps of your first scenario as a step in subsequent scenarios. Instead of the normal step definition, where you have "name of step" on line 1 and "definition of step" on line 2, you have:

function doStep1() { ...
function doStep2() { ...
function doStep3() { ...
function doStep4() { ...

this.Then('foo', function() { doStep1() };
this.Then('bar', function() { doStep2() };
this.Then('baz', function() { doStep3() };
this.Then('qux', function() { doStep4() };

function combinedSteps1to4() {
    doStep1();
    doStep2();
    doStep3();
    doStep4();
}
this.then('first step of scenario #2', function() { combinedSteps1To4()}
this.then('first step of scenario #3', function() { combinedSteps1To4(); combinedSteps5to8()}
// ...

So, I know this sounds like something only a non-programmer would want, but as a programmer of 10+ years I think this would actually be more beneficial to programmers than to people who want to create real logic in Gherkin syntax.

@floribon
Copy link
Contributor

floribon commented Sep 9, 2016

+1, it's in Cucumber so to me there's no reason to want things differently for its JS port

Edit: Didn't know that was considered anti pattern by so many people. No strong feeling (nor need) for this then, I'll give the devs who experienced it the benefit of the doubt.

@charlierudolph
Copy link
Member

See this comment as this has been suggested a few times and cucumber/cucumber-ruby#401.

@aslakhellesoy @mattwynne do you still see this as an anti pattern?

@machineghost
Copy link
Author

@charlierudolph While the concept of invoking an existing step has been mentioned in the comment/issue you mentioned, they seemed to cover a lot of ground, including calling steps from other steps in Gherkin. My request is JS-only, and as such I believe it addresses some of the previous concerns.

@jbpros
Copy link
Member

jbpros commented Sep 10, 2016

Yes we still consider this an anti pattern. It encourages treating steps as scripting instructions and a more imperative language in some steps which results in higher coupling between steps, reduced maintainability and other negative effects. All use cases for calling steps from steps I've seen so far are better addressed with other approaches.

@machineghost an improved regular expression would probably be the solution to A. I don't understand your point in B, can you please elaborate?

@machineghost
Copy link
Author

machineghost commented Sep 12, 2016

@jbpros Thanks for the reply. However, I don't think an improved regular expression solves A. In my example:

this.Then('whatever', function() {
    doSomething();
});
this.Then('something', function() {
    doSomething();
});

there are two entirely distinct steps. Now, obviously that's just an example; in a real test you wouldn't have two identical steps. Which leads me to B ...

So, in my experience at least, it's extremely common to repeat earlier steps in later steps. Test 1 is login, test 2 is login and go view some page, test 3 is login, view some page, then edit something etc. Now you can split that common logic for "login" and "go view some page" out in two ways.

  1. you can make split them out in to helper functions
  2. you can define them as steps and then re-use those steps

If you choose the latter then your steps read like this:

this.then('login', function() {
     // do login stuff
}
this.then('view page', function() {
    this.callStep('login'); // calls logic three lines up
    // do view page stuff
}
this.then('edit page', function() {
    this.callStep('view page'); // calls logic four lines up
    // do edit page stuff
}

If you use helper methods, then you instead have:

// helpers
function login() { /* do login stuff */}

// more lines of helpers

// steps
this.then('view page', function() {
    loginHelper(); // calls logic many lines removed
    // and realistically the helper is probably in a separate file
}

So by re-using steps you have a series of steps, each relying on the logic of a step a few lines up. If you want to see how it works, if you want to change anything, etc. everything relevant is right there. If you separate everything out in to functions you lose that, which in my opinion makes re-using steps within a step definition (but NOT within a Gherkin test definition) has value.

P.S. I'd also like to second @floribon's point; Cucumber proper has this:

https://github.com/cucumber/cucumber/wiki/Calling-Steps-from-Step-Definitions

You don't want Javascript to lose to Ruby do you? Those people can't even spell else if ;-)

@mattwynne
Copy link
Member

From the Ruby perspective, while I understand why people want this feature, I often regret that we have it. I think factoring out the automation helper code using something like the screenplay pattern is a much better idea in the long run.

@charlierudolph
Copy link
Member

@machineghost going to close as this doesn't have enough support from the cucumber core team.

Some workarounds if you'd like them.

and that's just when you do it once (the more you repeat the more bloat you get), without passing anything around (obviously a real world example would be even more bloated as at the very least you have to pass this all over the place).

Something I've seen is to have you step definitions merely parse the step argument and then call a function you have defined on your world instance. This way all the real logic and state can live in the world instance and thus you shouldn't be passing anything around. The world instance can call out to helpers to keep its size down, but it should the one holding all the state.

So, in my experience at least, it's extremely common to repeat earlier steps in later steps. Test 1 is login, test 2 is login and go view some page, test 3 is login, view some page, then edit something etc. Now you can split that common logic for "login" and "go view some page" out in two ways

This sounds like test 3 covers everything and if you have a failure in test 1, then test 2 and 3 will fail. If you have a failure in test 2, then test 2 and 3 will fail. Thus I think you could get away with just test 3. This sounds like a full integration test and I think its better to have just a few happy path integration tests and more unit tests / test at lower levels to catch all the edge cases

I haven't needed steps to call other steps, as a lot of times I just repeat the step in the gherkin. You gave an example of "view page" which calls the "login" step. And "edit page" which calls "view page". Instead I would have the step definitions do just their specific logic and repeat the step in the gherkin if its part of the process.

@lock
Copy link

lock bot commented Oct 25, 2018

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked as resolved and limited conversation to collaborators Oct 25, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants