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

jest.spyOn does not work when compared ts-jest #3843

Closed
Lycolia opened this issue Nov 1, 2021 · 11 comments · Fixed by #3845
Closed

jest.spyOn does not work when compared ts-jest #3843

Lycolia opened this issue Nov 1, 2021 · 11 comments · Fixed by #3845

Comments

@Lycolia
Copy link

Lycolia commented Nov 1, 2021

Problem

  • Doesn't work test when another function is called inside function.
  • But this works with ts-jest.
  • I understand that can do the same by injecting it as a callback function.

index.ts

export const child = () => {
  console.log('Hello World!');
};

export const callChild = () => {
  child();
};

index.spec.ts

import * as index from '.';

describe('index', () => {
  it('called', () => {
    const spiedChild = jest.spyOn(index, 'child');
    index.callChild();

    expect(spiedChild).toHaveBeenCalled();
  });
});

ts-jest

jest.config.js

module.exports = {
  clearMocks: true,
  coverageDirectory: 'coverage',
  coverageProvider: 'v8',
  preset: 'ts-jest',
  clearMocks: true,
  testEnvironment: 'node',
  roots: ['<rootDir>/src/'],
  moduleFileExtensions: ['ts', 'js'],
  collectCoverageFrom: ['src/**/*.{ts,js}'],
  testPathIgnorePatterns: ['<rootDir>[/\\\\](node_modules|dist)[/\\\\]'],
  watchPlugins: [
    'jest-watch-typeahead/filename',
    'jest-watch-typeahead/testname',
  ],
  silent: false,
};

Result

> jest

  console.log
    Hello World!

      at child (src/index.ts:4:11)

 PASS  src/index.spec.ts
  index
    √ called (29 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.603 s, estimated 3 s
Ran all test suites.

@swc/jest

jest.swc.config.js

module.exports = {
  clearMocks: true,
  coverageDirectory: 'coverage',
  coverageProvider: 'v8',
  transform: {
    '^.+\\.(t|j)sx?$': ['@swc/jest'],
  },
  clearMocks: true,
  testEnvironment: 'node',
  roots: ['<rootDir>/src/'],
  moduleFileExtensions: ['ts', 'js'],
  collectCoverageFrom: ['src/**/*.{ts,js}'],
  testPathIgnorePatterns: ['<rootDir>[/\\\\](node_modules|dist)[/\\\\]'],
  watchPlugins: [
    'jest-watch-typeahead/filename',
    'jest-watch-typeahead/testname',
  ],
  silent: false,
  extensionsToTreatAsEsm: ['.ts', '.tsx'],
};

Result

> jest -c jest.swc.config.js

  console.log
    Hello World!

      at child (src/index.ts:7:13)

 FAIL  src/index.spec.ts
  index
    × called (22 ms)

  ● index › called

    expect(jest.fn()).toHaveBeenCalled()

    Expected number of calls: >= 1
    Received number of calls:    0



      at Object.<anonymous> (src/index.spec.ts:30:28)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        0.408 s, estimated 1 s
Ran all test suites.
@descampsk
Copy link

descampsk commented Dec 9, 2021

Hello,

Any news or workaround to be able to use jest.spy on with swc ?

It sadly blocks to use this amazing work 😢

@wi-ski
Copy link

wi-ski commented Dec 15, 2021

Stuck on this now.

@develra
Copy link

develra commented Jan 13, 2022

I was able to get this working by using this pattern:
Before describe:

jest.mock("some_dependency", () => {
  const actualModule = jest.requireActual("some_dependency");
  return {
    __esModule: true,
    ...actualModule,
  };
});

In test:

const whateverSpy = jest.spyOn(some_dependency, "whateverMethodToSpyOn");

I only needed to use this when using the import * pattern like import * as some_dependency from "some_dependency"

@Birch-san
Copy link

Birch-san commented Mar 2, 2022

I think I've found the problem.

we need to look at the transformed code output by @swc/jest, and compare it to what's output by ts-jest.

you can see the transformed code by putting a conditional breakpoint in Jest, here:
https://github.com/facebook/jest/blob/3fbf2da6b2c26bc625e9a5d78e9ab46b527a786d/packages/jest-transform/src/ScriptTransformer.ts#L646
set the condition to: filename.includes('index.ts'), to see the file mentioned in this example).

recall that our original index.ts looks like this:

export const child = () => {
  console.log('Hello World!');
};

export const callChild = () => {
  child();
};

ts-jest transforms it into something like this…
note: what I'm showing here is not verbatim compiler output, but rather a manually-typed illustration based on the compiler output I'm seeing on a similar project.

'use strict';
Object.defineProperty(exports, "__esModule", { value: true });
exports.child = exports.callChild = void 0;
const child = () => {
  console.log('Hello World!');
};
exports.child = child;

const callChild = () => {
  // the `exports.` qualification is critically important for making this reassignable by external integrators!
  exports.child();
};
exports.callChild = callChild;

whereas @swc/jest transforms it with a subtle difference:

'use strict';
Object.defineProperty(exports, "__esModule", { value: true });
exports.child = exports.callChild = void 0;
const child = () => {
  console.log('Hello World!');
};
exports.child = child;

const callChild = () => {
  // note the lack of `exports.` qualification -- this means it will always invoke the real implementation
  child();
};
exports.callChild = callChild;

this means that index.ts#callChild will always invoke the genuine index.ts#child, even if a module from outside imported index.ts and mutated the module that it exports.

I think you could get around this by spinning the child function off into a separate file, so that index.spec.ts and index.ts mutually integrate against the same exported module.

but yes this is a fixable bug, rooted in how swc transforms ESM to CommonJS.

@Birch-san
Copy link

Birch-san commented Mar 2, 2022

to be clear, my recommended workaround (until the bug is fixed) is to split index.ts into two files:

child.ts

export const child = () => {
  console.log('Hello World!');
};

index.ts

import { child } from './child';
export const callChild = () => {
  child();
};

and change index.spec.ts like so:

import { callChild } from '.';
import * as childModule from './child';

describe('index', () => {
  it('called', () => {
    const spiedChild = jest.spyOn(childModule, 'child');
    callChild();

    expect(spiedChild).toHaveBeenCalled();
  });
});

this ensures that both index.ts and index.spec.ts integrate against child.ts's exports.
index.spec.ts reassigns child.ts#exports.child when it invokes jest.spyOn, and index.ts gets affected because it's sourcing child from that same structure.

@kdy1 kdy1 transferred this issue from swc-project/jest Mar 4, 2022
@kdy1 kdy1 added this to the v1.2.149 milestone Mar 4, 2022
@kdy1 kdy1 assigned kdy1 and unassigned kdy1 Mar 4, 2022
@kdy1 kdy1 closed this as completed in #3845 Mar 4, 2022
@kdy1 kdy1 removed this from the v1.2.149 milestone Mar 7, 2022
@kdy1
Copy link
Member

kdy1 commented Mar 7, 2022

Reopening as the patch is reverted, and the patch was wrong.

@kdy1 kdy1 reopened this Mar 7, 2022
@Austaras
Copy link
Member

Austaras commented Mar 8, 2022

I have runned a local test on my machine and can confirm ts-jest could work fine.

However, if I use babel and babel/preset-typescript to transform source code, or use native esm as tsc output result(which involves lots of manual config), tests would fail. babel output same result as swc, and native esm export all cannot be directly spied on.

So I suggest not to rely on specific implmentation of compiler.

@kdy1
Copy link
Member

kdy1 commented Mar 8, 2022

If native esm mode fails, jest.spyOne working with ts-jest is simply a bug of tsc.
Closing as swc is doing the correct thing.

@kdy1 kdy1 closed this as completed Mar 8, 2022
@Birch-san
Copy link

Okay yeah, it does seem like tsc treats ESM->CommonJS conversion differently:

tsc output

const y = () => {
    (0, exports.x)();
};

babel output

const y = () => {
  x();
};

This quirk of tsc is what makes this type of spying possible in ts-jest.

But yeah, if the goal of "converting ESM to CommonJS" is "preserve the guarantees of ESM", then the exported module is supposed to be considered immutable anyway. you wouldn't be able to do this type of spying on a real ESM module.

So, the "workaround" (extract into separate modules functions upon which you wish to spy) is probably the only solution left.

@ctsstc
Copy link

ctsstc commented Apr 27, 2022

Kind of confused what the resolution to this issue is exactly.

Tried what @develra mentioned like so:

import * as Router from 'next/router';

jest.mock('Router', () => {
  const actualModule = jest.requireActual('next/router');
  return {
    __esModule: true,
    ...actualModule,
  };
});

But then I get this error:

Cannot find module 'Router' from 'test-file.test.tsx'

Previously I've had this and it working, but something must have recently changed in a dependency...

import * as Router from 'next/router';

const useRouterSpy = jest.spyOn(Router, 'useRouter');\
useRouterSpy.mockReturnValue({ isReady: true });

@swc-bot
Copy link
Collaborator

swc-bot commented Oct 16, 2022

This closed issue has been automatically locked because it had no new activity for a month. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@swc-project swc-project locked as resolved and limited conversation to collaborators Oct 16, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Development

Successfully merging a pull request may close this issue.

9 participants