0

I am having a problem whereby if I export * from submodule (using ES6 module syntax and babel) I am unable to mock the submodules functions using Jest from the entry point. I wondered if anyone out there could help...

For example given this structure:

+ __tests__
|    |- index.js
+ greeter
|    |- index.js
|    |- submodule.js
|- index.js

And this code:

index.js

import { sayHello } from "./greeter";

export const greet = (name) => sayHello(name);

greeter/index.js

export * from "./submodule.js";

greeter/submodule.js

export const sayHello = (name) => console.log(`Hello, ${name}`);

__tests__/index.js

import { greet } from "../index";
import * as greeter from "../greeter";

describe("greet", () => {
    it("Should delegate the call to greeter.sayHello", () => {
        const name = "John";

        greet(name);
    });
});

This all works fine and when the test runs it passes. Hello, John is printed to the console as expected. The advantage that make this worth it to me is that index.js is completely unaware of the structure of the greeter module, so i can restructure and refactor that code without worrying about my consumers.

The Rub comes when I try and mock out greeter.sayHello...

__tests__/index.js

import { greet } from "../index.js";
import * as greeter from "../greeter";

greeter.sayHello = jest.fn();

describe("greet", () => {
    it("Should delegate the call to greeter.sayHello", () => {
        const name = "John";

        greet(name);

        expect(greeter.sayHello).toHaveBeenCalledWith(name);
    });
});

Now instead of the test passing as expected - I get an error:

Test suite failed to run

TypeError: Cannot set property sayHello of [object Object] which only has a getter
...(stack trace)

Changing the greeter import in __tests__/index.js to:

import * as greeter from "../greeter/submodule";

Makes the test pass but puts the coupling back in my test code.

Is there another way?

jaybeeuu
  • 1,013
  • 10
  • 25
  • It's perfectly ok. Tests are supposed to be tightly coupled. If you don't need to re-export sayHello for anything but tests, it should be imported from greeter/submodule in tests. – Estus Flask Apr 03 '18 at 11:16
  • @estus: thanks for the comment. I gently disagree about it being ok for the tests being tightly coupled. I don't want my tests to become a blocker to refactoring so making tests for one module (`index.js`) dependent on the internal structure of another (`greeter`) is bad. I am importing `sayHello` in `index.js` but i don't want to get it directly from `./greeter/submodule` (see my previous comment about refactoring.) so it must be re-exported for "production" as well as "test". – jaybeeuu Apr 03 '18 at 12:05
  • I judge from my own experience and decent code bases (frameworks, etc). If you do some drastic changes in code base, it's expected that tests will fail at some point, otherwise you can end up with tests that are too loose (if you don't want them to break, use IDE refactoring features that can fix imports automatically). This depends on how exactly Babel transpiles modules but I'm not sure that you will be able to mock this export when re-exporting it from `greetings`. Btw, jest.mock is the preferable way to mock exports. – Estus Flask Apr 03 '18 at 12:16
  • I think we're at risk of getting into a religious debate so I think I'll leave the coupling thing. I'm not aware of how to use `jest.mock` to mock out named exports (I've seen it done for `default`) could you point me at an example? – jaybeeuu Apr 03 '18 at 15:47
  • I'd say it's more practical than religious in this case.You can't mock separate module exports, only the module entirely (see https://github.com/facebook/jest/issues/936 ). I guess it's `jest.mock('./greeter', () => ({ sayHello: () => jest.fn() }) )`. You can't unmock `greeter.sayHello = jest.fn()` when it's done manually, while module mocks can be handled by Jest per test. – Estus Flask Apr 03 '18 at 16:14
  • Sorry estus, but your approach doesn't seem to work - if i swap out my assignment of `jest.fn()` for your `jest.mock` call when I perform the `expect` throws an error as the function is not a mock. My from what I can see in the docs `jest.mock` mocks the `default` export not named exports. In fact in your link cpojer indicates FB use the same approach as me to do their mocking of named exports. – jaybeeuu Apr 03 '18 at 17:08
  • This doesn't work in terms of 'default' exports or ES module exports at all. jest.mock falls back to Node `require` to keep module imports dynamic. This means that it mocks `*` for ES module imports. I'm not sure why this doesn't work in your case, possibly deserves a separate question with provided way to replicate the problem. I guess, at least this should be `jest.mock('../greeter'` instead of `'./greeter'`. – Estus Flask Apr 03 '18 at 17:41
  • Yeah, when I tested your implementation I changed the import to be correct. The question has been asked and answered here: https://stackoverflow.com/questions/40465047/how-can-i-mock-an-es6-module-import-using-jest. – jaybeeuu Apr 04 '18 at 08:43

1 Answers1

0

In order to mock a imported method on the file you want to test you need make sure the mock ran before you import your file(index.js), like this:

// import { greet } from "../index.js";   =====> Import on each test case(it)
// import * as greeter from "../greeter"; =====> Mock on each test case(it)

// greeter.sayHello = jest.fn(); =====> Would be not good to do this, it will mess with the entire import and this jest.fn will be share across your tests making it gives you false positives.

describe("greet", () => {
    it("Should delegate the call to greeter.sayHello", () => {
        const name = "John";

        jest.mock('../greeter', () => ({ sayHello: jest.fn() })); // here you mock the import, remember it needs to be before you require your index.js

        const greet = require('../index.js').greet; // require after the mock

        greet(name);

        expect(greeter.sayHello).toHaveBeenCalledWith(name);
    });
});
Victor Castro
  • 267
  • 4
  • 10
  • 2
    Nice idea but Jest hoists the `jest.mock` so it will always be run before the imports. And for that reason you it doesn't make sense to put `jest.mock` anywhere but in the root scope of your tests. https://jestjs.io/docs/en/manual-mocks#using-with-es-module-imports – jaybeeuu Oct 26 '19 at 19:02