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

Need I really to define each Scenario with its dependent steps outside of the feature file ? #52

Closed
pedrowilkens opened this issue Feb 20, 2019 · 11 comments
Labels
enhancement New feature or request

Comments

@pedrowilkens
Copy link

pedrowilkens commented Feb 20, 2019

As im working with jest-cucumber for some time, i was asking myself if it is really usefull if i need to allways define the scenarios and its related steps in defineFeature callback because that leads to duplicated code.

If i want to avoid this i need a hack like this:

         const feature = loadFeature(featureFile, tagFilter)

         defineFeature(feature, test => {
             feature.scenarios.forEach((scenario) => {
                test(scenario.title, (stepName) => {
                    scenario.steps.forEach((step) => {
                        stepName[step.keyword](step.stepText, Steps.get(step.stepText))
                    })
                })
            })
        })

Is my understanding correct ? Because every Scenario must be defined with its steps, i need to define them more then once.. ?

If there is another way and i am wrong, i would be very happy to know about it

Btw im reading your source code, because im working on cucumbers "Background" problem. Since its not implemented yet, everyone just uses beforeEach instead right?!

@martotkd1
Copy link

martotkd1 commented Feb 20, 2019

@pedrowilkens
Copy link
Author

pedrowilkens commented Feb 20, 2019

And when i want to reuse the steps in other features ? Then i have to outsource them anyway... and their implementation should at least be in one place so i used this approuch at the beginning but it didnt solve the Problem. Thanks anyway.

For clarity: I use jest-cucumber in combination with puppeteer so I have to reuse a lot of the Steps all over the different features.

@pedrowilkens pedrowilkens changed the title Need I really to define Scenarios and steps outside of the feature file ? Need I really to define each Scenario with its dependent steps outside of the feature file ? Feb 20, 2019
@bencompton
Copy link
Owner

Then i have to outsource them anyway... and their implementation should at least be in one place so i > used this approuch at the beginning but it didnt solve the Problem. Thanks anyway.

I didn't quite understand what you meant here, so pardon me if this is what you already tried. If you define your step definitions inside of functions, you can place them in shared modules and import them in multiple places:

// shared-steps.js
	
export const givenIHaveXDollarsInMyBankAccount = given => {
  given(/I have \$(\d+) in my bank account/, balance => {
    myAccount.deposit(balance);
  });
};

export const thenMyBalanceShouldBe = then => {
  then(/my balance should be \$(\d+)/, balance => {
    expect(myAccount.balance).toBe(balance);
  });
};
// example.steps.js

import { thenMyBalanceShouldBe, givenIHaveXDollarsInMyBankAccount } from './shared-steps';

defineFeature(feature, test => {
  let myAccount;
		
  beforeEach(() => {
    myAccount = new BankAccount();
  });

  test('Making a deposit', ({ given, when, then }) => {
    givenIHaveXDollarsInMyBankAccount(given);

    when(/I deposit \$(\d+)/, deposit => {
      myAccount.deposit(deposit);
    });

    thenMyBalanceShouldBe(then);
  });
	
  test('Making a withdrawal', ({ given, when, then }) => {
    givenIHaveXDollarsInMyBankAccount(given);

    when(/I withdraw \$(\d+)/, withdrawal => {
      myAccount.withdraw(withdrawal);
    });		

    thenMyBalanceShouldBe(then);
  });
});

When you import shared functions like this, you will still need to invoke them in every step definition file, which you might consider to be duplicate code. The philosophy behind that is that it's more readable to see the steps shown in the test code in the same order they appear in the feature file, but not everyone may agree.

Btw im reading your source code, because im working on cucumbers "Background" problem. Since its > not implemented yet, everyone just uses beforeEach instead right?!

That's right.

@bencompton
Copy link
Owner

@pedrowilkens, your "hack" is actually an interesting idea. It should be possible to use something like that to build on top of the existing API and provide an option that is more similar to Cucumber for people who prefer that.

As an example of what could be possible, a file like this could get registered with Jest and would create the Jest tests automatically:

// scenarios.spec.js

import { wireStepsToFeatures } from 'jest-cucumber';

wireStepsToFeatures('../../features/**/*.feature', '../step-definitions/**/*.js'); 

This would be an example of how step definitions are defined:

export default ({ given, when, then }) => {
  let accountBalance = 0;

  given(/I have \$(\d+) in my account/, (amount) => {
    accountBalance += amount;
  });

  when(/I deposit \$(\d+)/, (amount) => {
    accountBalance += amount;
  });

  then(/my balance should be \$(\d+)/, (balance) => {
    expect(accountBalance).toBe(balance);
  });
};

It would have to grab all of the step definition files from the glob pattern and evaluate them with require. Not sure how well that would work with ts-jest, though. It would then have to do pattern matching similar to what Cucumber itself does to locate step definitions as it loops through scenario.steps.

The downside would be that the vscode-jest extension wouldn't work, and the tests wouldn't be as readable (in my opinion). It seems there are enough people who expect jest-cucumber to work like this that it might be worth doing, though.

@bencompton bencompton added the enhancement New feature or request label Feb 22, 2019
@pedrowilkens
Copy link
Author

pedrowilkens commented Feb 24, 2019

Yes, that would be interesting to do. It would have the downside that you have only 1 "test/spec" file and the features would be all in one test suite so the regexp filter system of jest would not be applicable to all separated features.

I don't know which way it could be implemented in Typescript since I'm learning Typescript right now.
That's the way I did it in javascript in a Project. I know its more of a hack I was lazy so that was my solution

Defined Steps:

// general.steps.js
import Steps from './lib/Steps'

Steps.add(/^I enter "([^"]*)"' into the input field$/, async (text) => {
  await page.type('.inputfield', text)
})

export { Steps }

If you want to seperate the steps in different files.

// index.steps.js
import { Steps } from './general.steps'
import { Steps as specialSteps } from './special.steps'
import { Steps as otherSteps } from './other.steps'

Steps.concat(...specialSteps, ...otherSteps)

export { Steps }

Steps class which adds the step functions with its corresponding regexp

// Steps.js
class Steps {
  constructor() {
    this.list = []
  }

  // add steps and check if they are defined already 
  add(regExp, func) {
    const isAlreadyAdded = this.list.find((step) => String(step.regExp) === String(regExp))
    if (isAlreadyAdded) {
        throw Error(String(regExp) + ' : Step is already added. Please remove duplicate.')
    }
    this.list.push({regExp, func})
  }

  // If there are more step files they can be concatenated
  concat(...lists){
    this.list = this.list.concat(...lists)
  }

  // checks if the corresponding step function is defined and returns it
  get(title){
    let params
    const found = this.list.find((step) => {
      const matches = title.match(step.regExp)

      if (matches) {
          params = matches.slice(1)
          return true
      }

      return false
    })

    if (!found) {
        throw Error(title + ' : was not defined in Steps file')
    }

    return found.func.bind(this, ...params)
  }
}

export default new Steps()

The function which generates the tests

//  wireStepsToFeatures.js
import { defineFeature, loadFeature } from 'jest-cucumber'

export function wireStepsToFeatures(featureFile, Steps) {

    const feature = loadFeature(featureFile, tagFilter)

    defineFeature(feature, test => {
        feature.scenarios.forEach((scenario) => {
            test(scenario.title, (stepName) => {
                scenario.steps.forEach((step) => {
                    stepName[step.keyword](step.stepText, Steps.get(step.stepText))
                })
            })
        })
    })

The test file which wires them up.

// file.test.js
import { Steps } from './index.steps'
import { wireStepsToFeature } from './wireStepsToFeature.js'

wireStepsToFeature(featureFile, Steps) 

And the way someone would use it if it would be available through jest-cucumber

// general.steps.js
import { Steps } from 'jest-cucumber'

Steps.add(/^I enter "([^"]*)"' into the input field$/, async (text) => {
  await page.type('.inputfield', text)
})

export { Steps }
// file.test.js
import { Steps } from './general.steps'
import { wireStepsToFeature } from 'jest-cucumber'

wireStepsToFeature('tests/features/some.feature', Steps) 

I know that's not optimal but maybe there is a better way doing it in a more defensive way also I don't know how it behaves since you parse the featureFile and the scenarios in the feature file will be skipped duo tag filter? Also, it could just be another package like jest-cucumber-wired-up so it won't be confused with the normal way and it would force the package-user to think about the need of using it or not. I also think using it or not depends on how many steps need to be outsourced. If you heavily outsource the steps the benefit of defining the scenarios explicitly with references of actual steps is low because the implementation of the steps is not at the same place as the Scenario definitions (in the test/spec file).
So the information it contains becomes just a duplicate of the scenarios defined with its steps in the feature file. Another Benefit is that you concentrate more on the feature file and the consistency of independent steps since the implementation of the tests is unimportant for the stakeholder.
In conclusion, I would say maybe it should be a separate package that builds on top of jest-cucumber.
Because it's a special use case. It could also force the user to concentrate on the cucumber (feature) part of the process and then he would easily see if he needs to outsource a lot of the steps or not.

In my case, I needed to do this. Because I have 10 comprehensive feature files. And I reuse a lot.

@bencompton
Copy link
Owner

Nice, thanks for sharing!

I think that wireStepsToFeature would be a nice start to do one feature at a time, and then maybe wireStepsToFeatures could be the next phase to automatically wire all steps to all features quickly and easily, and have it basically work like Cucumber. What you have here looks good. One idea is that the steps class could expose more keywords. For example, add could be renamed to defineStep, and given, when, then, and, but could be exposed that end up calling defineStep.

I know that's not optimal but maybe there is a better way doing it in a more defensive way also I don't know how it behaves since you parse the featureFile and the scenarios in the feature file will be skipped duo tag filter?

Tag filters should be fine. The only thing is that you're hiding defineFeature, which has the option to specify tag filters and other configuration parameters. wireFeatureToSteps could have the ability to specify those configuration parameters instead and then pass them through, or it could just require using the external configuration file instead.

If you heavily outsource the steps the benefit of defining the scenarios explicitly with references of actual steps is low because the implementation of the steps is not at the same place as the Scenario definitions (in the test/spec file).
So the information it contains becomes just a duplicate of the scenarios defined with its steps in the feature file. Another Benefit is that you concentrate more on the feature file and the consistency of independent steps since the implementation of the tests is unimportant for the stakeholder.

Yes, I definitely agree that the jest-cucumber syntax is less useful when there are a lot of duplicated steps in the feature files.

The current syntax is designed with the assumption that there aren't a lot of duplicated steps. I often find that too many duplicated steps in a feature file is a "code smell" that suggests the language in the feature files is too low-level. In my experience, describing high-level business logic in feature files (e.g., given I have 2 products in my cart) instead of lower-level UI interactions (i.e., click this, then click that, etc.) results in a ton less duplicate steps. Each step ends up with specific and unique usage of domain language instead of repeating knowledge that already exists in other steps. In my projects, I tend to end up with a few steps being duplicated within the same feature files, but rarely duplicated across feature files. One reason is that cross-cutting concerns (e.g., logging in) are typically described in one feature file and then implied everywhere else. For example, if you're looking at your account details in an app, then you're obviously logged in. Using "screen" and "workflow" abstractions described in books like "The Secret Cucumber Ninja Scrolls" greatly reduces duplication in the implementation code as well.

Also, it could just be another package like jest-cucumber-wired-up so it won't be confused with the normal way and it would force the package-user to think about the need of using it or not

Agreed, a separate library would keep things lean and avoid confusion. It shouldn't run into problems, since it calls the public API and doesn't really need access to the internals. Maybe the docs about avoiding duplicate steps could include a link to the new library.

@axten
Copy link

axten commented Jun 4, 2019

Hi, are there any updates here?
We have actually the same problems using jest-cucumber...
We have a small set of steps but want to setup hundreds of gherkin scenarios by QA members that don't know JS very well...

@bencompton
Copy link
Owner

I think it's worth implementing in jest-cucumber directly, since this is such a common request. This will be next in line after #27 is completed.

@ronnyek
Copy link

ronnyek commented Nov 1, 2019

Any word on this stuff? As it sits, (unless I am missing something huge) it seems like the way steps have to be defined (contextually tied to a specific scenario), it seems like jest-cucumber defeats some of the purpose of cucumber. (bdd testing seems to be particularly well suited for allowing non software engineers to use prebuilt steps to build tests)

@slimee
Copy link

slimee commented Feb 21, 2020

Hi @bencompton @pedrowilkens did you advance any on this work?
Regards, thanks for this lib.
Merging #67 would be awesome

@bencompton
Copy link
Owner

I ended up implementing automatic step binding to address this, which will go out in a release tomorrow. See the docs and examples for more info.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

6 participants