Cucumber-js: An option to relax the arguments check for the step definitions

Created on 12 Feb 2016  ·  14Comments  ·  Source: cucumber/cucumber-js

Hi, Though I do understand the value of the strict checking that is being done for the step arguments, I also feel that there is need to pass this control back to the users.

For example, in my case, I want to implement generic wrapper functions around the step implementation functions where I want to add some generic processing logic before and after the step invocation. For this generic wrapper, I need to access just whatever arguments were passed and hence need access to the arguments array rather than declaring any explicit parameters. Currently, cucumber would just stop me from doing this as it does a strict check of parameters.

I would like to propose adding another configuration parameter to the options object called maybe something like skipStrictParameterCheck which if not set would be assumed as false. That way for most common usage the default behaviour would be strict checks but for others who want to use the framework to build something more around it, it gives them the flexibility to leverage some of the dynamic capabilities of JavaScript.

All 14 comments

I made the same request, see #445 :)

@riaan53 , yeah I did have a look at that and unfortunately the request was rejected due to perhaps some of the right reasons. While I dont disagree with the reasoning, I do think that as part of building a larger framework, this feature is important as long as users are not abusing it, hence my recommendation to make this a strictly optional piece of configuration.

What is some of this generic processing logic you are talking about?

Sure. In one use case, I want all developers to be able to use expression references in their step definitions. e.g. 'When the user ${admin} logs in to the system'.

Now, in this case ${admin} would be resolved maybe from a JSON file and the responsibility for resolution would be with the developer while implementing the step definition. if you really look at such kind of property resolution though, this can be done by generic code with the developer not being aware of it at all.

For allowing this, I can easily create a generic function wrapper around the developer step implementations which would accept the raw arguments injected by Cucumber, resolved them and then inject the resolved values into the actual step implementation.

Currently, I would not be able to write such a generic function as Cucumber's validations would make sure that the function has the right number of parameters which in my case wont be as my generic wrapper function would accept no named arguments and I would be using the 'arguments' object.

Hope I could make sense. There are other use cases as well in our current project where a generic wrapper function around the step implementations would be needed

If we implemented step argument tranforms would that solve your problem: https://github.com/cucumber/cucumber/wiki/Step-Argument-Transforms?

Ah yes, could potentially address the reference resolution problem. However, I still have the need to put a generic wrapper function around the step implementations for other reasons.

For example one reason is, I am using Protractor with Cucumber as the framework and in Protractor, one has to register custom promises in the WebDriver control flow so that it waits diligently for these custom promises to get resolved before proceeding to the next step.

While Cucumber does wait for a promise to get resolved (if returned from the step implementation), it obviously is not WebDriver aware and most often we need to add additional code to each step implementation to register the returned promise with WebDriver.

This again is a very easy thing to address through a generic function wrapper for which the parameter checking has to be relaxed by Cucumber.

I ran into this issue a couple of times now.

The most prominent one is implementing a retry helper on every Then step:

cucumber.Then = function(match, callback) {
  const retryingCallback = (...args) => retry(async () => await callback(...args));
  cucumber.Then(match, retryingCallback);
};

I use this helper to deal with timing issues on an eventually consistent backend. Essentially, it executes the callback every x seconds until either y seconds have passed or the callback passes.

Sadly, this causes

function has 0 arguments, should have 1 (if synchronous or returning a promise) or 2 (if accepting a callback)

The alternative that I'm using now is to call the helper in every Then step, which causes a LOT of code duplication.

this.Then(/^I see that "([^"]*)" does not have a destination$/, async clientName => {
  return retry(async () => {
    const client = await homeView.clientByName(clientName);
    expect(client.destinationName).to.not.exist;
  });
});

Another case is a more simple one where I want a helper function for login.

function loggedIn(username, func) {
  return (...args) => {
    await accounts.login(username);
    return func(...args)
  };
}

this.Then(/^someone logged in as "([^"]*)" sees a destination named "([^"]*)"$/, loggedIn(assert.destinationExists));

Also this one would save me a lot of code duplication.

Finally, I expect at some point to want to add a suite that runs all my acceptance tests, but restarts the server before every Then callback (to be sure server restarts don't mess things up). Again, LOTS of duplication.

P.S. The retry helper:

const patience = 250;
const interval = 5;

function delay(time) {
  return new Promise(function (fulfill) {
    setTimeout(fulfill, time);
  });
}

async function attempt(start, func) {
  const attemptDate = new Date();
  try {
    return await func();
  } catch (errr) {
    const timeElapsed = attemptDate.getTime() - start.getTime();
    if (timeElapsed < patience) {
      await delay(interval);
      return await attempt(start, func);
    } else {
      throw errr;
    }
  }
}

export async function retry(func) {
  const start = new Date();
  return await attempt(start, func);
}

_Edit_

Tried to hack my way around it:

function splat(func) {
  return (one, two, three, four, five, six, seven, eight, nine, ten) => {
    if (typeof ten !== 'undefined') {
      return func(one, two, three, four, five, six, seven, eight, nine, ten);
    } else if (typeof nine !== 'undefined') {
      return func(one, two, three, four, five, six, seven, eight, nine);
    } else if (typeof eight !== 'undefined') {
      return func(one, two, three, four, five, six, seven, eight);
    } else if (typeof seven !== 'undefined') {
      return func(one, two, three, four, five, six, seven);
    } else if (typeof six !== 'undefined') {
      return func(one, two, three, four, five, six);
    } else if (typeof five !== 'undefined') {
      return func(one, two, three, four, five);
    } else if (typeof four !== 'undefined') {
      return func(one, two, three, four);
    } else if (typeof three !== 'undefined') {
      return func(one, two, three);
    } else if (typeof two !== 'undefined') {
      return func(one, two);
    } else if (typeof one !== 'undefined') {
      return func(one);
    } else {
      return func();
    }
  };
}

cucumber.Then = function(match, callback) {
  const retryingCallback = splat((...args) => retry(async () => await callback(...args)));
  cucumber.Then(match, retryingCallback);
};

but

function has 10 arguments, should have 1 (if synchronous or returning a promise) or 2 (if accepting a callback)

making me a sad panda

Here is an example of wrapping a function to retain the length. Would be nice if there was just a tiny node module that did this for you.

Thanks for the suggestion! That works if you specify the number of arguments per step definition, right?

eg.

this.Then(/^someone logged in as "([^"]*)" sees a destination named "([^"]*)"$/, createProxy(loggedIn(assert.destinationExists), 2));

The most pressing issue for me is not being able to add generic middleware for multiple step definitions. Something like createProxy could maybe work if I made it into a object that allows middleware registration, and then tell it the number of arguments at every single step definition. (look closely at my first example and you'll see I can't directly use createProxy, because the retry function will wrap it. It should be the other way around, but then createProxy won't know the number of arguments for each callback)

Still feels really awkward though, compared to being able to switch the error off. :innocent:

I think you can use a variant of that function where instead of passing in proxyLength you just pass in the function you are wrapping and use function.length.

Great suggestion, thanks!

I got something like the following to work:

cucumber.Then = function(match, callback) {
  cucumber.Then(match, retryProxy(callback));
};

function retryProxy(func) {
  const numberOfArgs = func.length;
  switch (numberOfArgs) {
    case 0: return () => retry(func);
    case 1: return (a) => retry(func, a);
    case 2: return (a, b) => retry(func, a, b);
    case 3: return (a, b, c) => retry(func, a, b, c);
    case 4: return (a, b, c, d) => retry(func, a, b, c, d);
    case 5: return (a, b, c, d, e) => retry(func, a, b, c, d, e);
  }
}

Two things it doesn't solve is the login helper case and allowing default parameters, but I can work around those two.

Glad I can now add middleware with no adjustments to my step definitions!

@thomasvanlankveld Just so you know, I found this library which wraps a function to give it a specific function length: https://github.com/blakeembrey/arity

@sushil-rxr could you something like in your generic function wrapper to retain the original function length?

In 2.0.0-rc.1 you can now add a generic function wrapper. It also has the built in functionality of retaining the original function length.

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