swc: Incorrect Jest coverage

When using @swc/jest to transpile ts to commonjs and running jest --coverage certain branches are shown as not covered (console logging in these branches show that tests do run the code path). Using babel to transpile and run the tests shows the correct coverage.

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 51
  • Comments: 68 (17 by maintainers)

Commits related to this issue

Most upvoted comments

I added this to my Jest configuration in package.json which seemed to help.

"jest": {
    "collectCoverage": true,
    "transform": {
      "^.+\\.(t|j)sx?$": [
        "@swc/jest",
        {
          "sourceMaps": true
        }
      ]
    }
  }

@kdy1 Hm, I tested both sourceMaps in .swcrc and as an option in the jest.config.json. Both did not work for me. Even with target: "es2021".

For my NestJS project, I also noticed that test coverage dropped quite a bit when switching from ts-jest to @swc/jest.

Digging into it, it seems to be related to how the Typescript metadata for constructors are transpiled.

Take this constructor:

constructor(
  @Inject(AppService)
  private readonly appService: AppService,
) {}

The output of swc for the design:paramtypes metadata for the constructor is:

_ts_metadata("design:paramtypes", [
    typeof _appservice.AppService === "undefined" ? Object : _appservice.AppService
])

Notice the typeof ternary. This gets flagged as an “uncovered branch” by istanbul, the coverage calculator used by jest.

Screen Shot 2023-06-30 at 8 16 17 AM

Compare this to how ts-jest outputs this:

__metadata("design:paramtypes", [app_service_1.AppService])

No ternary, so no impact to code coverage.

I created a workaround that tells istanbul to ignore these lines that incorrectly impact code coverage. It inserts an /* istanbul ignore next */ comment in the necessary places, resulting in transpiled code like this:

_ts_metadata("design:paramtypes", [
    /* istanbul ignore next */typeof _appservice.AppService === "undefined" ? Object : _appservice.AppService
])

I setup this repo with instructions to reproduce: https://github.com/rogisolorzano/nest-swc-coverage

The workaround I’m using is in create-swc-transformer.js

Curious if there are any thoughts on this and potential solutions that don’t involve having to insert /* istanbul ignore next */ into the transpiled output by swc?

I think https://github.com/swc-project/swc/pull/7900 may improve the situation.

@pspeter3 Thank you for your code snippet, it helps, but now there is another problem:

It looks like not covered code highlights are not displayed correctly.

There is an example of coverage report from project with:

  • jest@27.3.1
  • @swc/core@1.2.118
  • @swc/jest@0.2.11

image

the yellow highlight here says that the comma is supposedly not covered

@krutoo ran into the same issues and guessed it is caused by the conversion to older JS syntax. We could fix these false-positives by targeting a newer ES version.

  transform: {
    '^.+\\.(t|j)sx?$': [
      '@swc/jest',
      {
        jsc: {
          target: 'es2021',
        },
        sourceMaps: true,
      },
    ],
  },

Our issue was that components that were using emotion were being tested but not captured in the coverage reporting. Using sourceMaps: 'inline' was all it took to get coverage back up to where it was.

I saw mention of using @swc-node/jest vs @swc/jest what are the differences?

I’m using NestJS as well and this seems to be related to decorators, so removing the legacyDecorator and/or decoratorMetadata is not an approach I can test.

This seems to be very related to an issue ts-jest was/is having here, maybe there’s some insight to be taken from there? If they did resolve it I think it was via a similar method as rogisolorzano provided which is adding istanbul ignore comments.

I think I’ve tried all configuration permutations listed here so far without any luck, as well as installing @swc-node/jest & @swc/helpers and trying it as the jest transformer. One thing to note to keep you sane is that you may need to run npx jest --clearCache between configuration changes it seems before running your next jest coverage run (I noticed removing the transform decorators in my swcrc config broke tests, and still showed they were broken after reverting everything ?!).

Potentially Related Issues on Other Projects

Ultimately it sounds like source mapping can help some scenarios, changing the target in other scenarios, but specific decorators don’t seem to care about either of those in some scenarios like Angular and NestJS commonly it seems.

I took some time to checkout the related “fix for decorator coverage” that I linked above. It does seem they took the same approach as @rogisolorzano 👍

I’m not sure if this project has a way to implement a “pre-processor” but maybe that’s something @rogisolorzano could open a PR for is this is how folks seem to be getting around this. Otherwise it sounds like there needs to be a major rework on the system to implement so that it doesn’t require a ternary (or is this an underlying transpilation implementation issue of these frameworks with how the decorator is written?).

https://github.com/kulshekhar/ts-jest/pull/488/files#diff-0548fd42c45d69a916cd9689c388d2c60b74746b4b7626a008167273557b4e57

PS: Thanks @rogisolorzano for the minimal reproduction and digging deep into this.

@klutzer are you not using Jest? I believe that’s the scope of this issue, but I believe the coverage issue is not directly related to jest but the coverage tool [istanbul].

I had the same issue. But when I changed sourceMaps: true to sourceMaps: "inline", jest started to collect coverage correctly.

My config is:

// jest.config.js
const config = {
  transform: {
    "^.+\\.(t|j)sx?$": ["@swc/jest", { ...swcConfig, sourceMaps: "inline" }],
  },
}

Ofc, Adding the option to .swcrc works as well.

Hope this helps people.

@kdy1 Here it is: https://github.com/sarneeh/swc-project-swc-issues-3854

When I was creating it I noticed that the false-positives go away when I turn off decoratorMetadata option (which is unfortunately required when using dependency injection frameworks like typedi). Hopefully that will help you somehow 🙏

Also found that unused exports are marked as uncovered statements:

image

Removing export makes them covered.

I tried:

  • externalHelpers: true
  • decorators: false
  • target: "es2021"
  • sourceMaps: true
  • in different combinations.

Unfortunately, it does not help to restore the coverage and even also makes it worse.

Useing coverageProvider: 'v8' will probably solve the problem. Solution funded at stackoverflow https://stackoverflow.com/a/74851858/8770040

I’ve tested with "sourceMaps": "inline" but didn’t work.

.swcrc:

{
  "sourceMaps": "inline",
  "jsc": {
    "target": "es2021",
    "parser": {
      "syntax": "typescript",
      "decorators": true,
      "dynamicImport": true
    },
    "keepClassNames": true,
    "transform": {
      "legacyDecorator": true,
      "decoratorMetadata": true
    }
  }
}

package.json:

"devDependencies": {
    ...
    "@swc/core": "^1.3.20",
    "@swc/jest": "^0.2.23"
},
"jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      "ts"
    ],
    "rootDir": ".",
    "testRegex": ".*\\.spec\\.ts$",
    "transform": {
      "^.+\\.(t|j)s$": "@swc/jest"
    },
    "collectCoverageFrom": [
      "src/**/*.{ts,js}"
    ],
    "moduleNameMapper": {
      "^@app/(.*)$": "<rootDir>/src/$1",
      "^@test/(.*)$": "<rootDir>/test/$1"
    },
    "coverageDirectory": "./coverage",
    "coverageReporters": [
      "json-summary",
      "text",
      "lcov"
    ],
    "coverageThreshold": {
      "global": {
        "branches": 100,
        "functions": 100,
        "lines": 100,
        "statements": 100
      }
    },
    "testEnvironment": "node"
  },
  "engines": {
    "node": "^16.16.0"
  }
}

I’ve noticed that jest coverage will report code branches inside helper functions as being uncovered lines unless the externalHelpers:true options is specified.

          jsc: {
            externalHelpers: true,
          },

Adding this option improved my test coverage significantly since the helper code is no longer included in the coverage scan. I’m not sure if anyone else has encountered this?

I opened a PR to add this option in @swc-node/jest but it probably needs further investigation/discussion https://github.com/swc-project/swc-node/pull/673

The clue about decoratorMetadata really helps! I already fixed codegen and it means some transform is dropping source map information, and now I know which pass is doing so. Thank you!

yes, can confirm. This is still not fixed. But /* istanbul ignore next */ now works 😃

You need sourceMaps: true or sourceMaps: "inline". I verified that it’s working

My specific problem seems to have been resolved by https://github.com/swc-project/swc-node/pull/673, for what it’s worth.

I’m actually using @swc-node/jest which already sets sourcemaps to inline by default.

This is true… 😐

I had same issue. Could you show swcConfig at here?

Sure. @960590968 Here are my config files for testing. And I’m using React. Basically, swcConfig is configs from .swcrc

jest.config.js
const { readFileSync } = require("fs");

const swcConfig = JSON.parse(readFileSync(`${__dirname}/.swcrc`, "utf-8"));

/** @type {import('jest').Config} */
const config = {
  rootDir: ".",
  collectCoverage: true,
  collectCoverageFrom: [
    "src/**/*.{ts,tsx}",
    "!src/**/*.stories.tsx",
  ],
  coverageThreshold: {
    global: {
      statements: 90,
      branches: 90,
      functions: 90,
      lines: 90,
    },
  },
  setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
  transform: {
    "^.+\\.(t|j)sx?$": ["@swc/jest", { ...swcConfig }],
  },
  testEnvironment: "jsdom",
  testMatch: ["<rootDir>/src/**/*.test.{ts,tsx}"],
};

module.exports = config;

jest.setup.js
import "@testing-library/jest-dom";

.swcrc
{
  "sourceMaps": "inline",
  "minify": true,
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "jsx": true
    },
    "transform": {
      "react": {
        "runtime": "automatic"
      }
    }
  }
}
Dependencies

// package.json

{
  ...
  "dependencies": {
    "clsx": "^1.2.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@storybook/addon-essentials": "^6.5.13",
    "@storybook/addon-postcss": "^2.0.0",
    "@storybook/builder-vite": "^0.2.5",
    "@storybook/react": "^6.5.13",
    "@swc/core": "^1.3.20",
    "@swc/jest": "^0.2.23",
    "@testing-library/dom": "^8.19.0",
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^13.4.0",
    "@testing-library/react-hooks": "^8.0.1",
    "@testing-library/user-event": "^14.4.3",
    "@types/jest": "^29.2.3",
    "@types/node": "^18.11.9",
    "@types/react": "^18.0.25",
    "@types/react-dom": "^18.0.9",
    "autoprefixer": "^10.4.13",
    "dprint": "^0.33.0",
    "husky": "^8.0.0",
    "jest": "^29.3.1",
    "jest-environment-jsdom": "^29.3.1",
    "lint-staged": "^13.0.4",
    "npm-run-all": "^4.1.5",
    "postcss": "^8.4.19",
    "storybook-addon-themes": "^6.1.0",
    "tailwindcss": "^3.2.4",
    "typescript": "^4.9.3"
  }
}

Yes, it will definitely help. Ideally if I can invoke jest to get coverage for single file, I can test/ensure that jest reports 100% coverage

@kdy1 I tested the new version. Unfortunately, its still not fixed 😦

Tested with es2020 & es2021 + sourceMaps: true & sourceMaps: "inline". I updated all files in my gist build with v1.2.155: https://gist.github.com/bobaaaaa/3649b3a7e6312793a257bf67c500128a

(Keep in mind, this is not a huge blocker/issue for us. Still thx for looking into this)

Hey @kdy1 my reported issue here is now fixed for me with "@swc/jest": "0.2.22"

@sebald Thank you, looks like it realy works with target: "es2021"