bencompton / jest-cucumber

Execute Gherkin scenarios in Jest
Apache License 2.0
655 stars 115 forks source link

Hooks and autoBindSteps #92

Open slimee opened 4 years ago

slimee commented 4 years ago

Thanks for this lib, I would like to use it everywhere :)

Also, is there some hooks available "after all given", and "after all when"? This is useful to trigger fixtures or request after they have been prepared by given/when

bencompton commented 4 years ago

From your previous title, it sounds like you're using autoBindSteps, which currently doesn't support the same notion of hooks as Cucumber.js. autoBindSteps is somewhat of a MVP at the moment, and the only way to work around this for now is to perform setup in your actual steps, as shown in the example.

I haven't really found the perfect way to accomplish hooks yet. Here's one option:

export const vendingMachineHooks: Hooks = (beforeEach, afterEach, beforeAll, afterAll) => {
  beforeEach(() => {
    // Perform setup that applies to every scenario in all feature files
  });

  beforeEach('@vending-machine-with-one-item', () => {
     // Perform setup that applies to scenarios with the specified tag
  });
};
import { vendingMachineSteps } from 'specs/step-definitions/vending-machine-steps';
import { vendingMachineHooks } from 'specs/hooks/vending-machine-hooks';

const features = loadFeatures('specs/features/**/*.feature');
autoBindSteps(features, [ vendingMachineSteps ], [ vendingMachineHooks ]);

I generally prefer the way hooks like beforeEach work in Jest tests over the way hooks work in Cucumber, because, for example, beforeEach can be easily used to instantiate classes for each specific test, whereas the process isn't as clean and simple with Cucumber hooks. So, whenever I use other Cucumber-like libraries such as SpecFlow, I just end up performing setup in my given steps instead of hooks anyway. Therefore, I'm not super happy with the idea above. I suppose one advantage of the approach above is that you can have beforeEach hooks that are specific to a subset of your feature files. For example, you can do this:

import { vendingMachineSteps } from 'specs/step-definitions/vending-machine-steps';
import { vendingMachineHooks } from 'specs/hooks/vending-machine-hooks';
import { arcadeSteps } from 'specs/step-definitions/arcade-steps';
import { arcadeHooks } from 'specs/hooks/arcade-hooks';

const vendingMachineFeatures = loadFeatures('specs/features/vending-machine/**/*.feature');
autoBindSteps(vendingMachineFeatures, [ vendingMachineSteps ], [ vendingMachineHooks ]);

const arcadeFeatures = loadFeatures('specs/features/arcade/**/*.feature');
autoBindSteps(arcadeFeatures, [ arcadeSteps ], [ arcadeHooks ]);

This assumes that all scenarios within a set of feature files require the same hooks, which might not always be the case.

slimee commented 4 years ago

Thanks you for your answer, it makes sense.

In my case, the hook are the same for all the project. Our need is just this: A simple callback (always the same!) called after all givens of a test were executed, and another one called after all whens.

In the first callback, I will put the loading of the fixtures prepared in all the givens. In the second, I will put the server call with the request prepared in all the whens.

But I don't know how to do that. Any advice? I am ready to participate Regards

bencompton commented 4 years ago

As far as I am aware, even Cucumber doesn't support hooks before or after individual steps, and instead hooks run before or after entire scenarios. This is a bit of a unique case, and one idea that comes to mind is to wrap your step definition functions to include your setup. Perhaps something close to this would work for you:

const wrapStep = (defineStep, setupFunction) => {
  return (stepMatcher, stepFunc) => {
    const wrappedStepFunc = async (...args) => {
       await setupFunction();
       await stepFunc.apply(null, args);
    };

    defineStep(stepMatcher, wrappedStepFunc);
  };
};

const performSetup = async () => {
  ...
};

export const steps = (unwrappedGiven) => {
  const given = wrapStep(unwrappedGiven, performSetup);

  given('some step text', () => {
    ...
  });
};
mainfraame commented 3 years ago

It seems like a lot of people are having issues with trying to reduce step declarations. @bencompton @slimee you should check out cucumber-jest. It's a jest transformer that lets you use cucumber with jest without the pitfalls of jest-cucumber

robert-skarzycki commented 3 years ago

@mainfraame, as commented on other issue (https://github.com/bencompton/jest-cucumber/issues/95#issuecomment-777650916), it would be fair to uncover a fact that you are "marketing" your library here (you are a contributor of cucumber-jest). IMO there is no point in just saying "try another tool" without specifying what problem new tool solves that the old one doesn't.

MetaMmodern commented 3 years ago

@bencompton Hello Ben. I was looking for a solution to implement a beforeEach function for every .feature file and this issue was the only one where you discuss a beforeEach hook inside step definitions. But I can't use it inside step definitions, because they may be called many times or not called at all, and I need the callback to be called before every feature file call. Is there any workaround in your library without changing the source code? Thanks in advance (I am interested in afterEach also)

bencompton commented 3 years ago

For autoBindSteps, there currently is no notion of hooks, whereas beforeEach, etc. work outside of autoBindSteps. I never found the elegant solution I was hoping, so I suppose simple tag-based hooks should be the plan. Hooks are an important gap to make autoBindSteps usable, and they also may have use cases outside of autoBindSteps as well (e.g., global hooks for tagged scenarios to eliminate duplicate beforeEach blocks).