Best kept Jest secret: Testing only changed files with coverage reports

UPDATE: this whole post is soon (https://github.com/facebook/jest/pull/5601) no longer necessary and the same thing can be done using: jest --coverage --changedSince=master

If you’re using Jest today and run your entire testing suite on every PR (Pull Request) to generate code coverage reports, then stop what you’re doing. Spending 5 minutes to read this post will be the best investment you’ve done all summer!

On the surface the advice to use coverage reports on pull requests and to only test changed files might seem at odds with each other.

Au contraire! It’s just a well kept secret how the magic happens.

If you want to ensure a certain coverage threshold on every PR, without needing to run your entire testing suite, Jest got us covered.

This post will guide you through these steps:

  1. How to list all changes between two git branches that are relevant to Jest.
  2. Using Jest APIs to find every related test.
  3. Have Jest exclude unchanged files from the code coverage report.

What changed

Lets use execa so that we can easily chain our cli commands.

First we ask git to tell us which files have changed between the base branch (remotes/origin/develop) and our own branch (on travis the name is TRAVIS_BRANCH, you get the idea).

Then we get just the .js files using grep so we filter out files that aren’t tested.

const execa = require("execa");
const branch = process.env.CIRCLE_BRANCH;
execa
.shell(
`git diff --name-only remotes/origin/master...${branch} | grep .js$`
)
.then(({ stdout, stderr }) => {
if (stderr) {
throw new Error(stderr);
}
return stdout.replace(/\n/g, " ");
});

Find every related test

The —-findRelatedTests is one of the best features in Jest. Combined with --listTests and 🎉, you have a complete list of every test that is related to your changed files.

Adding this code snippet to the previous one:

.then(changedFiles =>
execa
.shell(
`jest --listTests --findRelatedTests ${changedFiles}`
)
.then(({ stdout, stderr }) => {
if (stderr) {
throw new Error(stderr);
}
return {
changedFiles: changedFiles.split(" "),
relatedTests: JSON.parse(stdout)
};
})
);

changedFiles will be used to tell Jest what we want to check for testing coverage. relatedTests will be used to decide what tests to run.


Running the tests

Using --collectCoverageFrom we’re switching Jest from checking every js file in your project, to just the changed ones.

At the end we simply tell it what tests to run.

.then(ctx => {
if (ctx.relatedTests.length < 1) {
// wow, nothing to test!
return;
}
const collectCoverageFrom = ctx.changedFiles
.map(from => `--collectCoverageFrom "${from}"`)
.join(" ");
const testFiles = ctx.relatedTests
.map(testFile => path.relative(process.cwd(), testFile))
.join(" ");
const coverageCommand = `jest --coverage ${collectCoverageFrom} ${testFiles}`;
return execa
.shell(coverageCommand, { stdio: "inherit" })
.catch(() => {
process.exitCode = 1;
});
});

That’s all there is to it 😄.

Full example

const execa = require("execa");
const path = require("path");
const branch = process.env.CIRCLE_BRANCH;
execa
.shell(
`git diff --name-only remotes/origin/master...${branch} | grep .js$`
)
.then(({ stdout, stderr }) => {
if (stderr) {
throw new Error(stderr);
}
return stdout.replace(/\n/g, " ");
})
.then(changedFiles =>
execa
.shell(
`${jest} --listTests --findRelatedTests ${changedFiles}`
)
.then(({ stdout, stderr }) => {
if (stderr) {
throw new Error(stderr);
}
return {
changedFiles: changedFiles.split(" "),
relatedTests: JSON.parse(stdout)
};
})
)
.then(ctx => {
if (ctx.relatedTests.length < 1) {
// wow, nothing to test!
return;
}
const collectCoverageFrom = ctx.changedFiles
.map(from => `--collectCoverageFrom "${from}"`)
.join(" ");
const testFiles = ctx.relatedTests
.map(testFile =>
path.relative(process.cwd(), testFile)
)
.join(" ");
const coverageCommand = `jest --coverage ${collectCoverageFrom} ${testFiles}`;
return execa
.shell(coverageCommand, { stdio: "inherit" })
.catch(() => {
process.exitCode = 1;
});
});