bcoe / c8

output coverage reports using Node.js' built in coverage
ISC License
2k stars 91 forks source link

0% coverage for files only containing TypeScript interfaces #494

Open Xerillio opened 1 year ago

Xerillio commented 1 year ago

I'm running c8 using mocha with ts-node/register and I'm wondering if that might cause the issue. Essentially, I have a file task-result.ts that only contains a ITaskResult interface. This is used from task-runner.ts (import { ITaskResult } from "./task-result"). I have tests with 100% coverage for task-runner.ts, but using the --all flag, c8 reports 0% coverage for task-result.ts.

If I move the interface into task-runner.ts instead, task-runner.ts still reports 100% coverage as expected.

Is this an artefact from TS interfaces not being transpiled to JS and/or related to the same underlying issue in #182? Or is there a way to solve it while keeping the interface in its own file (I'd prefer not having to exclude individual files like this)?

// .c8rc.json
{
    "all": true,
    "src": ["./src"],
    "extension": [".ts"],
    "report-dir": "./coverage"
}

// package.json (snippet)
    "scripts": {
        "test": "mocha -r ts-node/register -r mocha-suppress-logs './tests/**/*.test.ts'",
        "coverage": "c8 npm test"
    },

If necessary, I can create a small example to reproduce it.

bcoe commented 1 year ago

@Xerillio could you please provide a reproduction for this issue.

Xerillio commented 1 year ago

@bcoe You got it 😉

/package.json:

{
  "scripts": {
    "test": "mocha -r ts-node/register './tests/**/*.test.ts'",
    "coverage": "c8 npm test"
  },
  "devDependencies": {
    "@types/chai": "^4.3.9",
    "@types/mocha": "^10.0.3",
    "c8": "^8.0.1",
    "chai": "^4.3.10",
    "mocha": "^10.2.0",
    "ts-node": "^10.9.1",
    "typescript": "^5.2.2"
  }
}

/tsconfig.json:

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "esModuleInterop": true,
    "strict": true
  }
}

/.c8rc.json:

{
    "all": true,
    "src": ["./src"],
    "extension": [".ts"],
    "report-dir": "./coverage"
}

/src/power.ts:

import type { IPower } from "./ipower";

export class Power implements IPower {
  calculate(base: number, exponent: number): number {
    if (!Number.isInteger(exponent)) throw new Error("The exponent must be an integer");

    let result = 1;
    for (let i = 0; i < exponent; i++) {
      result *= base;
    }

    return result;
  }
}

/src/ipower.ts:

export interface IPower {
  calculate(base: number, exponent: number): number
}

/tests/power.test.ts:

import { Power } from "../src/power";
import { expect } from "chai";

describe("Power", () => {
  describe("#calculate", () => {
      it("should return 1 when the exponent is 0", () => {
          const sut = new Power();

          const result = sut.calculate(9.876, 0);

          expect(result).to.equal(1);
      });

      it("should throw when the exponent is not an integer", () => {
          const sut = new Power();

          expect(() => sut.calculate(9.876, 1.1)).to.throw(
              Error,
              "The exponent must be an integer");
      });
  });
});

Running coverage then produces this result (0% for ipower.ts):

$ npm run coverage
...
-----------|---------|----------|---------|---------|-------------------
File       | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files  |   70.58 |       60 |      50 |   70.58 |
 ipower.ts |       0 |        0 |       0 |       0 | 1-3
 power.ts  |   85.71 |       75 |     100 |   85.71 | 9-10
-----------|---------|----------|---------|---------|-------------------

Funny thing I noticed now because I didn't add tests for 100% coverage... if you move the contents of ipower.ts into power.ts the coverage of power.ts is strangely enough higher than before:

$ npm run coverage
...
----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files |    87.5 |       75 |     100 |    87.5 |
 power.ts |    87.5 |       75 |     100 |    87.5 | 7-8
----------|---------|----------|---------|---------|-------------------
ericmorand commented 1 year ago

Is there news about this issue?

jftanner commented 4 days ago

I encounter this too on a regular basis.

My go-to solution for now is to put my interfaces in one directory and then exclude that whole directory:

- src/
  - interfaces/
    - widget.ts
  - classes/
    - WidgetFactory.ts
- test/
  - WidgetFactory.suite.ts
- .c8rc.yaml
{
  "all": true,
  "src": "./src",
  "exclude": [
    "interfaces/"
  ]
}

Or, if you prefer to co-locate your tests and interfaces, you could use file extensions:

- src/
  - widgets/
    - widget.interface.ts
    - WidgetFactory.spec.ts
    - WidgetFactory.ts
- .c8rc.yaml
{
  "all": true,
  "src": "./src",
  "exclude": [
    "**/*.interface.ts",
    "**/*.spec.ts"
  ]
}

However, both of these share the problem that it's possible to end up with uncovered code if someone accidentally (or intentionally) puts something other than an interface in that directory/file. So, I'd love to see a solution that could somehow recognize and ignore these files automatically.

ericmorand commented 4 days ago

@jftanner could please give One Double Zero a try?

https://www.npmjs.com/package/one-double-zero?activeTab=readme

jftanner commented 3 days ago

@jftanner could please give One Double Zero a try?

No offense, but I'm not interested in switching to such a new tool. A large part of security in open source comes from having many eyes looking at it. At 11 downloads per week, your tool isn't there yet.

ericmorand commented 3 days ago

That makes no sense right? How can a tool get downloads if you don't download it because it doesn't have downloads.

It solves your issue. It is fully tested. It is open source. What you want more?