FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

hamodywe/ghosttest: Counts the tests in your repository, then counts the ones that actually run — and tells you where the difference went. · GitHub

ghosttest

How many of your tests actually run?


The problem

Somebody writes user.spec.ts. Your Jest config says testMatch: ["**/*.test.ts"].

The file is never collected. The tests inside it have never run — not once, not in CI, not locally. The suite passes. Coverage does not drop, because uncovered files were never in the denominator. Code review sees a file full of tests and approves it.

Nothing anywhere reports this. There is no warning, no error, no summary line. Jest cannot tell you about a file it does not know exists.

The same thing happens four other ways:

  • a .only left behind after debugging silently switches off every other test in that file — and the run still says PASS;
  • an ignore pattern added for one directory quietly swallows another;
  • a test moved outside roots;
  • a test that asserts nothing and therefore cannot fail.

Every one of these is invisible from a green build. That is what makes them expensive: the build is not just failing to catch bugs, it is actively reporting that it checked.

ghosttest counts the tests in your repository, counts the ones that will run, and tells you where the difference went.

What it looks like

$ ghosttest

13 tests found · 4 will run · 9 never will
  █████████▎                      31%

  5 in files never collected · 3 suppressed by a focused test · 1 skipped
  runner: jest — collection rules read exactly from JSON (package.json)

error   uncollected-test-file legacy/cart.test.js:1
  2 tests in this file, and jest would not collect it — it is excluded by `/legacy/`.
  so: These tests have never run. The suite passes without them, so nothing has
      ever reported that this code is untested — which is worse than having no
      tests, because the green tick says otherwise.
  fix: Narrow the exclude pattern. Exclusion is applied after matching, so a
       broad entry silently beats a correct include.

error   focused-test-committed src/checkout.test.js:2
  A focused test on line 2 silently switches off the other 3 tests in this file.

Two numbers, side by side. That is the whole product.

Install

npx ghosttest

Or as a dev dependency:

npm install --save-dev ghosttest

Requires Node 20.10 or newer. Zero dependencies — including the glob matcher, which had to be written by hand because Jest's default testMatch uses three extglob forms in one line.

Use it

ghosttest                        # current directory
ghosttest ./packages/api         # somewhere else
ghosttest --verbose              # list every test file and its case count
ghosttest --json                 # machine-readable
ghosttest --fail-on warning      # stricter gate

In CI, before or after your tests — it does not need them to have run:

- run: npm ci
- run: npx ghosttest

Exit codes: 0 clean, 1 findings at or above the threshold, 2 bad usage.

Supported runners

Runner Rules read from
Jest the jest field in package.json, jest.config.json (exact), or literal arrays in jest.config.{js,ts,mjs,cjs}
Vitest literal include/exclude in vitest.config.* or vite.config.*
node --test the glob in your test script

Each runner's documented defaults are used when no configuration is found, and the report always says which of the two it did — "read exactly from JSON" and "assumed from the runner's defaults" support very different levels of confidence.

Three things it gets right

A test file is a file that contains tests. Not a file whose name matches a pattern. Defining it by name would inherit the exact bug the tool exists to find — user.spec.ts is a test file by any human standard, and it is precisely the one nobody is running. So every source file is read and scanned, and the ones with test cases in them become the census.

The cost of a .only is counted, not just flagged. Lint rules tell you not to commit one. ghosttest tells you that the one on line 2 switched off the other seventeen tests in that file. The second is what gets it fixed.

Nothing is executed. vitest.config.ts is a TypeScript module that can do anything; a tool that imports it to read two arrays has quietly become a tool that runs the repository it was pointed at. Config files are parsed as text. When a config is computed rather than literal, ghosttest says so and falls back to documented defaults instead of guessing.

The rules

Rule Severity Meaning
uncollected-test-file error a file full of tests the runner never collects
focused-test-committed error a .only suppressing the rest of its file
test-without-assertion warning a test that runs and cannot fail
skipped-test-inventory info how many tests are skipped, and where

Details, with worked examples of each: docs/rules.md.

Try it

The haunted fixture contains one small project with all four problems:

git clone https://github.com/hamodywe/ghosttest && cd ghosttest
npm install
node src/cli.ts test/fixtures/haunted   # 13 tests, 4 run
node src/cli.ts test/fixtures/clean     # silence

It counts itself correctly

$ npx ghosttest .
82 tests found · 82 will run

$ npm test
ℹ pass 82

The census and the runner agree exactly. That is the check that matters for a counting tool, and it runs in CI on every commit.

Fixture directories

A repository that deliberately contains test files nobody should run — this one does — declares them:

{ "ignore": ["test/fixtures/**"] }

in ghosttest.config.json, or with a repeatable --ignore flag. There are no built-in exclusions for directories called fixtures: silently ignoring a directory is exactly the failure this tool exists to find, and building one in would be a poor joke.

Limitations

Stated plainly.

  • It counts declarations, not executions. it.each([...]) is one declaration that becomes many tests at runtime; ghosttest counts it once. A loop that generates tests is counted once too.
  • Dynamically generated tests are invisible. If your suite builds test names from a file listing, static analysis cannot see them.
  • Computed configs fall back to defaults. A jest.config.js that builds testMatch from a function is reported as "assumed from defaults", which may be wrong for your project. The report says so rather than hiding it.
  • testRegex support is approximate. Jest allows both globs and regular expressions; ghosttest guesses which language a pattern is written in from its syntax, and the guess is not perfect.
  • Monorepos are scanned one package at a time. Point it at each package.
  • Assertion detection is heuristic. It recognises expect, assert, should, chai, sinon and t.is-style calls, and errs toward assuming a test does assert. A custom assertion helper may be missed.

FAQ

Isn't this what jest --listTests does? --listTests prints what Jest collects. It cannot print what it should have collected, because it does not know those files exist. The value here is the comparison — and --listTests is a manual debugging step nobody runs on a schedule.

Doesn't ESLint catch the .only? eslint-plugin-jest/no-focused-tests and Biome's noFocusedTests do, if you have them configured for your test files — which many projects using Vitest do not. ghosttest also counts what the .only suppressed, which the lint rules do not.

Will it work without installing my project's dependencies? Yes. It reads files; it never invokes the runner.

Does it modify anything? No. It only reads.

Contributing

The most useful issue is a wrong census — a test counted that does not exist, or a file reported as uncollected that your runner does in fact collect. See CONTRIBUTING.md.

Roadmap

See ROADMAP.md.

Licence

MIT © hamodywe

References

About

Counts the tests in your repository, then counts the ones that actually run — and tells you where the difference went.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages


Back | FazBrowse Home | New Git URL