bencompton / jest-cucumber

Execute Gherkin scenarios in Jest
Apache License 2.0
657 stars 117 forks source link

Cannot mock `fs` module #20

Open bkostrowiecki opened 6 years ago

bkostrowiecki commented 6 years ago

When I try to mock fs module then feature files cannot be found and the jest throws the exception.

 FAIL  features/Create the hierarchy.feature.ts
  ● Test suite failed to run

    Feature file not found (/Users/user/project/features/Create the hierarchy.feature)

      at Object.<anonymous>.exports.loadFeature (node_modules/jest-cucumber/dist/src/parsed-feature-loading.js:151:15)
      at Object.<anonymous> (features/Create the hierarchy.feature.ts:5:108)
          at Generator.next (<anonymous>)
          at new Promise (<anonymous>)
          at Generator.next (<anonymous>)
          at <anonymous>
      at process._tickCallback (internal/process/next_tick.js:188:7)

In my opinion, it will be difficult to make it work with current implementation but I think it's worth a mention.

stand-sure commented 4 years ago

This approach works for me


const whenIRequestIt = () => {
        jest.mock("fs", () => ({
            readFile: jest.fn(
                (
                    path: string,
                    _: { [name: string]: any },
                    callback: CallbackShape
                ) => {
                    const shouldSucceed = path.endsWith(mockFileName);
                    const err = shouldSucceed ? null : "Not found";
                    const data = shouldSucceed ? mockFileContents : undefined;
                    callback(err, data);
                }
            ),
        }));

        const { functionBeingTested } = jest.requireActual( // this REPLACES the import at the top of the file
            "path/to/module"
        );
        const retVal = functionBeingTested (fileName)
            .then((t: string) => (text = t))
            .catch((e: any) => (err = e));

        jest.restoreAllMocks();

        return retVal;
    };

Earlier in the file...


    let fileName: string | undefined;
    let text: string | undefined;
    let err: any;

    beforeEach(() => {
        fileName = undefined;
        text = undefined;
    });
bencompton commented 3 years ago

Here's another perspective for anyone who happens across this thread. I generally prefer to avoid avoid jest.mock myself due its magical properties creating issues like the one in this thread, and also because it's generally considered better design to depend on abstractions instead of concretions, use dependency injection, and all that jazz. While I don't necessarily follow principles like SOLID dogmatically, I have found that using DI--especially with dependencies that manage I/O--really helps with building fast and stable integration tests, and can also have other beneficial effects.

Case in point, I'm currently working on integration tests for the Jest Cucumber library itself, and I refactored a bit so that Jest itself could be dependency injected and replaced with a mock test runner for integration tests. As a result, the integration tests are able to use Jest Cucumber to test itself, which would be really difficult to accomplish using jest.mock . The other benefit of "relying on abstractions instead of concretions" in this case is that it is also possible to inject not just a mock implementation of Jest for testing, but also other test runners aside from Jest. For example, I did a prototype this week that injected Mocha into Jest Cucumber, and actually it worked quite well to run Jest Cucumber tests with Mocha.

As another example, in React apps where I'm leveraging Jest Cucumber, most of my tests have Jest Cucumber testing the react-redux containers, which create a top-level API for the entire app, and then I use another of my libraries, I/O Source to dependency inject either a real HTTP service implementation, or a mock service implementation. Interestingly, this not only facilitates integration tests, but the same mock service implementations and test data can be used to run the app in "mock mode", which is great for demos, and is also great for development, since engineers can get the front-end flow perfected with mock services and data, and then worry about the back-end once the front-end is perfected.

EDIT:

If anyone is interested in the React app architecture I described above, I do have a tutorial that describes it detail how it all works. The tutorial is admittedly opinionated and based around my own OSS libraries, but the same concepts apply when using other libraries.

rquast commented 3 years ago

I agree and think DI gives more flexibility for both testing as well as use of code in other frameworks (like what you did with Mocha). However, I removed all the fs functionality from the experiment I did (fs/callsites/glob) because it won't run in a browser for web test runner (which requires its own readFile method to send the data to playwright/selenium/browserstack that runs the modified jest-cucumber code). Maybe providing an interface for loadFeature and loadFeatures and turning most of the code into a core package that gets implemented by jest-cucumber (like what cucumber did with the gherkin package as a monorepo). Also, Mocha has a few differences from Jest - you can't use test.concurrent with Mocha for example, and timeout is implemented differently too. For now I hacked around that by specifying the framework to use (https://github.com/rquast/gherkin-testkit/blob/5d048f413daaf0cddf962194d9ba52feb690bf59/src/feature-definition-creation.ts#L117). If you were to follow the monorepo pattern, another library, say "test-runner-cucumber" or even "mocha-cucumber" could add @jest-cucumber/core as a dependency and implement loadFeature & loadFeatures? This would help you retain the name jest-cucumber without having to change semantics (though it might be a painful move).

EDIT:

Maybe also an interface for getTestFunction so you can inject that to get around the differences?