Cucumber-js: Calling steps from step definitions

Created on 1 Jun 2011  ·  31Comments  ·  Source: cucumber/cucumber-js

Most helpful comment

:-1: :-1: :-1: :-1: :-1:

Calling steps from stepdefs is one of those features I wish I never added to Cucumber(-Ruby), because it provides so much rope for people to hang themselves with. It came to be because Ruby stepdefs use anonymous closures that you can't call from elsewhere (unless you go through hoops).

With JavaScript it's a different situation; Step Definitions use first class functions!

function x_is_something(x, cb) {
  console.log('x', x);
  cb();
}

Given(/x is (.*)/, x_is_something);

Given(/super x/, function(cb) {
  x_is_something(97, cb);
});

All 31 comments

When this is implemented, bear in mind that we plan to deprecate everything other than #steps for doing this in cucumber-rb. See https://github.com/cucumber/cucumber/issues/68

Thank you Matt. -js will only support steps() then.

I like it!

Any progress on this? Seems like a pretty vital feature.

This is not planned as part of the current milestone (0.3). It _should_ be part of 0.4.

@mattwynne I guess we want to support step() as well. Am I correct?

@jbpros I guess. Maybe you could start with #step. It is simpler to implement because you're just invoking a step rather than parsing Gherkin.

I personally would be happy to use a Cucumber without this feature, I never use it and consider it to be bad practice. I always prefer to delegate to methods instead.

Ultimately, if we're going to support this, I would prefer to see it implemented in Gherkin, so you have a way of defining a macro step that maps to a bunch of other low-level steps. Cucumber then just gets told to invoke the lower level ones and hardly needs to know there's any mapping going on. That would be my preference.

TL;DR: Should we really add steps()/step() to Cucumber.js (and -jvm, -ruby 2, etc.)?

I totally agree with you Matt. _Unfortunately_, this is the most wanted feature right now on Cucumber.js.

As I understand it, many people consider step definitions as _methods_ or _functions_. The way I see it, they're mappings between some fluent language sentences and pieces of programming language code, nothing more. This has deep implications as to how we treat those beasts.

From a programmer perspective, step definitions look just like methods though. I see it as a weakness in Cucumber_s_ today. Step definitions should not be exposed as an API but rather as an explicit translation map, a dictionary, so to speak.

@msassak had interesting thoughts on this already and I think he did a great job re-modeling those "mappings" in Cucumber 2.0.

I have to say I'm reluctant to work on this very issue in Cucumber.js right now. On the other hand I don't want to do feature retention just because of my personal tastes/opinions.

:-1: :-1: :-1: :-1: :-1:

Calling steps from stepdefs is one of those features I wish I never added to Cucumber(-Ruby), because it provides so much rope for people to hang themselves with. It came to be because Ruby stepdefs use anonymous closures that you can't call from elsewhere (unless you go through hoops).

With JavaScript it's a different situation; Step Definitions use first class functions!

function x_is_something(x, cb) {
  console.log('x', x);
  cb();
}

Given(/x is (.*)/, x_is_something);

Given(/super x/, function(cb) {
  x_is_something(97, cb);
});

CLOSED (WONTFIX) :hammer:

I'm not sure I understand how step() would be better than a simple JS function call here. Isn't that what it'd end up doing anyway, with an additional layer of indirection (i.e. a step definition to make a GET request to a specific user that still needs to be translated into a JS function)?

I noticed you wrote _since I have no way to trigger a scenario_, did you mean step or is that intentionally scenario (in the latter case, I can see what you're trying to do, I think).

You can still define users in a background and iterate over them in your When step.

Feature:
  Background:
    Given a valid user called Simon
    And a valid user called Sarah
    And a valid user called Johnny

  Scenario Outline:
    When each valid user sends a GET request to /search<query>
    Then everyone's request response code is 400
    And everyone's request response body starts with <body>

  Examples:
    | query  | body |
    | ?foo=1 | ...  |

And yes this means storing request responses in an array and iterating over them. Is it that bad?

Well, if you have some flow (for example checkout flow) and you want to get to the final confirmation page, you can go through steps, defined (somewhere) in the step definitions.
I agree that you can also define a function somewhere and invoke it from the step definition. But its minimize the effort of moving it from steps to function. If you described some flow somewhere in BDD, you don't need to spend additional time for moving it to separate library, you can simply invoke step definitions.

Invoking one single step is almost useless. I was thinking of porting all ruby's step(s) functionality here. But as my request closed, I would not spend time for it.

Thanks.

So sad this won't fix, as @cono says: invoking one single step is almost useles, real cases are more complex operations.

This would be really helpful to create a few fine-grained scenarios and then the others scenarios repeating this operations. Specially when the steps are defined in more than one file, in that case function-reuse alternative is not quite easy nor clean.

Hy! I've created a lib which does exactly what you are asking (call a step from another step), take a look here: https://github.com/hackhat/cucumberry
Feedback is welcome!

@hackhat It looks really cool. I usually like sync portion of step definition. Is cucumber-pro only a plugin for cucumber.js?

@jlin412 I don't really know how to call, but is like a helper for cucumber. Thanks for feedback

@hackhat In order to create sync step, I will need to use syntax: this.addStep(...)? Will I need to use selenium-sync as well instead of protractor.js/webdriver.js?

@jlin412 you can only use the sync plug-in, look at the docs how to do it. I'm on mobile so I can't give you the exact steps. If you can wait I will explain better around 23h30 Portugal hour.

@hackhat please change the name of your project to something else. See hackhat/cucumber-pro#1

@aslakhellesoy Changed the name to https://github.com/hackhat/cucumberry

@aslakhellesoy Then how to chain the step calls? Like following?

function x_is_something(x, cb) {
  console.log('x', x);
  this.x = x;
  cb();
}
function y_is_something(y, cb) {
  console.log('y', y);
  this.y = y;
  cb();
}

Given(/super z/, function(cb) {
  x_is_something(97, cb);
  y_is_something(8, cb);
});

That doesn't work very well since x_is_something would have called the callback before y_is_something has chance to finish its work.

And also, if step stores variable in the world context, it will end up each time call the function, we need to bind it like:

Given(/super z/, function(cb) {
  x_is_something.bind(this)(97, cb);
  y_is_something.bind(this)(8, cb);
});

Anyone had solutions to these issues?

You need to use async and use the parallel function. In this way you call
the main cb only after both of the sub calls have finished.
About binding you can either use this with bind or use a closure variable.

On Thu, May 14, 2015, 00:15 Yun Jia [email protected] wrote:

@aslakhellesoy https://github.com/aslakhellesoy Then how to chain the
step calls? Like following?

function x_is_something(x, cb) {
console.log('x', x);
this.x = x;
cb();
}function y_is_something(y, cb) {
console.log('y', y);
this.y = y;
cb();
}

Given(/super z/, function(cb) {
x_is_something(97, cb);
y_is_something(8, cb);
});

That doesn't work very well since x_is_something would have called the
callback before y_is_something has chance to finish its work.

And also, if step stores variable in the world context, it will end up
each time call the function, we need to bind it like:

Given(/super z/, function(cb) {
x_is_something.bind(this)(97, cb);
y_is_something.bind(this)(8, cb);
});

Anyone had solutions to these issues?


Reply to this email directly or view it on GitHub
https://github.com/cucumber/cucumber-js/issues/11#issuecomment-101845619
.

+1, this should be part of the lib.

What @mattwynne suggested (add a gherkin feature which supports code reusability):

    When a
    Then x
    When b
    Then y

---------------------------------

    Define x
        Then p
        And y
        And q

---------------------------------

    Step a
        ...
    Step b
        ...
    Step p
        ...
    Step q
        ...
    Step y
        ...

---------------------------------

What @aslakhellesoy suggested (extracting the duplicated code to js functions):

    When a
    Then x
    When b
    Then y

---------------------------------

    Step a
        ...
    Step b
        ...
    Step x
        ...
        Call f(p)
        ...
    Step y
        Call f(p)

---------------------------------

    Function f(p)
        ...

---------------------------------

Calling steps from step definitions

    When a
    Then x
    When b
    Then y

---------------------------------

    Step a
        ...
    Step b
        ...
    Step x
        ...
        Call y(p)
        ...
    Step y
        ...

---------------------------------

I still don't understand why do we need another unnecessary abstraction level, do you have any explanation?

@yunjia this is basic callback theory:

Given(/super z/, function(cb) {
  x_is_something(97, function () {
    y_is_something(8, cb);
  });
});

As for the binding issue, you should define those functions as methods on your World:

function World(callback) {
    this.x_is_something = function (x, callback) {
      this.x = ...
    };

    this.y_is_something = function (y, callback) {
      this.y = ...
    };

    callback(); // tell Cucumber we're finished and to use 'this' as the world instance
  };
}
module.exports.World = World;

Then in your step definitions:

Given(/super z/, function(cb) {
  var self = this;
  self.x_is_something(97, function () {
    self.y_is_something(8, cb);
  });
});

Also, please note that synchronous step definitions are supported by Cucumber.js 0.5+:

Given(/super z/, function() {
  this.x_is_something(97);
  this.y_is_something(8);
});

@inf3rno step definitions are a thin translation layer between plain english and JS code. By allowing calling steps from steps, we make that layer fatter. Steps become coupled between each other, rendering them extremely difficult to maintain.

It also encourages people to use Gherkin as a scripting language, which it is not at all.

@inf3rno if you want to reuse code in stepdefs, move the body of the stepdef to a regular javascript function and reuse that.

@jbpros

It also encourages people to use Gherkin as a scripting language, which it is not at all.

Can you elaborate pls. I don't understand what is the connection, since step definitions are not in gherkin, only the text pattern what is similar.

@inf3rno if you can call steps from steps, you're jumping back again into "Gherkinland": the step names need to be parsed, matched against step definitions which can be executed. You're basically writing gherkin scripts within gherkin (they're hidden within JS step definitions, but that's a detail that makes it even worse from a maintenance POV).

@aslakhellesoy @jbpros The idea is that steps should be an algebraic, composable type, which it's not.

@jbpros, I am going to use your solution since I am used to programming in Java and not worrying about promises :)

Did anyone ever come up with an extension to cucumber for defining steps in terms of other steps? Something like

Understand I log in to {site} as {user}, {pass}
    I visit {site}
    I log in as {user}, {pass}

Am pondering whether this is a useful extension to better describe long user journeys through the system and would prefer to work with any prior art.

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

edwinwright picture edwinwright  ·  3Comments

jfstephe picture jfstephe  ·  4Comments

jechazelle picture jechazelle  ·  5Comments

edgarechm picture edgarechm  ·  5Comments

kozhevnikov picture kozhevnikov  ·  6Comments