Testing in Nx Monorepos: Affected Commands, Task Pipelines, and Caching
Nx is one of the most popular monorepo build systems for JavaScript and TypeScript projects. Its key promise for testing is simple: only run tests for code that actually changed. When a monorepo contains dozens of applications and libraries, that promise can cut CI time from 45 minutes to under 5.
This guide covers nx affected, task pipeline configuration, and caching strategies for test suites in Nx workspaces.
How Nx Determines What Changed
Nx builds a project graph — a dependency map of every app and library in your monorepo. When you run nx affected, Nx computes which projects are affected by changes between two commits (or between your branch and main):
# Run tests only for affected projects
nx affected --target=<span class="hljs-built_in">test
<span class="hljs-comment"># Compare against a specific base branch
nx affected --target=<span class="hljs-built_in">test --base=main --<span class="hljs-built_in">head=HEAD
<span class="hljs-comment"># See what would be affected without running
nx affected --target=<span class="hljs-built_in">test --dry-runThe affected computation uses git diff. If lib-a is changed and app-b depends on lib-a, both are marked affected. Libraries deeper in the dependency graph propagate changes upward.
Project Graph Configuration
Nx infers the project graph from your tsconfig.json path mappings and package.json imports. For accurate affected detection, make sure library paths are declared correctly:
// tsconfig.base.json
{
"compilerOptions": {
"paths": {
"@myorg/ui": ["libs/ui/src/index.ts"],
"@myorg/data-access": ["libs/data-access/src/index.ts"],
"@myorg/utils": ["libs/utils/src/index.ts"]
}
}
}You can visualise the full project graph in the browser:
nx graphThis shows which projects would be caught by a change to any file — useful for debugging why a test is or isn't running.
Task Pipeline Configuration
Nx pipelines define execution order between targets. For testing, the most important relationship is that build often must complete before test:
// nx.json
{
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"outputs": ["{projectRoot}/dist"]
},
"test": {
"dependsOn": ["build"],
"inputs": ["default", "^default"],
"outputs": ["{projectRoot}/coverage"]
},
"lint": {
"inputs": ["default", "{workspaceRoot}/.eslintrc.json"]
}
}
}The ^build notation means "build all dependencies first". The inputs field tells Nx which files affect the cache key — changing a file not listed as an input won't invalidate the cache.
Nx Computation Cache
The cache is Nx's biggest CI win. When a task's inputs haven't changed, Nx replays the cached output instead of running the task:
# First run — actually executes tests
nx <span class="hljs-built_in">test my-lib
<span class="hljs-comment"># Second run with no file changes — instant cache hit
nx <span class="hljs-built_in">test my-lib
<span class="hljs-comment"># ✔ nx test my-lib [local cache]Cache is stored locally by default in node_modules/.cache/nx. For CI, you need remote caching to share the cache between pipeline runs and team members.
Nx Cloud Remote Cache
Nx Cloud provides remote caching with no infrastructure to manage:
# Connect workspace to Nx Cloud
nx connect-to-nx-cloudAfter connecting, every CI run reads from the shared cache. A branch that was already tested by another developer won't re-run its tests — it replays the cached results in milliseconds.
Running Tests with Nx
Nx wraps your existing test runner (Jest, Vitest, Karma) with its affected and caching layer. Configure the test executor in each project's project.json:
// libs/my-lib/project.json
{
"name": "my-lib",
"targets": {
"test": {
"executor": "@nx/jest:jest",
"options": {
"jestConfig": "libs/my-lib/jest.config.ts",
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
}
}
}For CI, add the ci configuration:
nx affected --target=test --configuration=ciThis enables --ci mode in Jest (no watch, clear output) and collects coverage.
Parallelism
Nx runs tasks in parallel by default, respecting pipeline dependencies. Control parallelism with --parallel:
# Run up to 4 test tasks in parallel
nx affected --target=<span class="hljs-built_in">test --parallel=4
<span class="hljs-comment"># Serial execution (useful for debugging flaky tests)
nx affected --target=<span class="hljs-built_in">test --parallel=1The default is 3. On CI machines with more CPUs, increasing this reduces wall-clock time. The pipeline constraints (dependsOn) are still respected — Nx won't run a test before its dependency is built.
Ignoring Files from Affected Analysis
Some files should never trigger affected test runs — documentation, changelogs, scripts that don't affect app behaviour:
// nx.json
{
"namedInputs": {
"default": [
"{projectRoot}/**/*",
"sharedGlobals"
],
"sharedGlobals": [
"{workspaceRoot}/babel.config.json",
"{workspaceRoot}/tsconfig.base.json"
],
"noJekyll": [
"!{projectRoot}/**/*.md",
"!{projectRoot}/**/*.txt"
]
},
"targetDefaults": {
"test": {
"inputs": ["default", "^default", "noJekyll"]
}
}
}Negation patterns (!) exclude files from the affected computation. Adding !**/*.md means editing a README doesn't trigger tests.
Distributed Task Execution
For very large monorepos, nx affected alone isn't enough — you need to split tasks across multiple CI agents. Nx Cloud's Distributed Task Execution (DTE) handles this:
# .github/workflows/ci.yaml
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
- run: npm ci
- run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js"
- run: npx nx affected --target=test --parallel=3 --configuration=ci
- run: npx nx-cloud stop-all-agentsDTE spins up agent machines, distributes affected tasks across them, streams output back to the main job, and terminates agents when done. The result is parallel test execution across machines without custom orchestration.
Coverage Merging
When tests run across multiple machines or projects, coverage reports need to be merged:
# Each project generates coverage in its own dir
<span class="hljs-comment"># Merge with nyc or c8
npx nyc merge coverage coverage/merged/coverage-final.json
npx nyc report --reporter=lcov --temp-dir=coverage/mergedAlternatively, use Nx's built-in coverage aggregation by pointing Istanbul/V8 at a combined output path in nx.json.
Practical CI Configuration
A full GitHub Actions workflow for Nx testing:
name: CI
on:
pull_request:
jobs:
affected-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set SHAs for affected
uses: nrwl/nx-set-shas@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- name: Run affected tests
run: npx nx affected --target=test --parallel=3 --base=$NX_BASE --head=$NX_HEAD
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
directory: coverage/The nrwl/nx-set-shas action sets NX_BASE and NX_HEAD correctly — it compares against the last successful CI run on the base branch, not just main, which avoids re-testing code that already passed.
Key Takeaways
nx affected --target=testruns only tests for changed projects and their dependents- The project graph must accurately reflect your import structure for affected detection to work
- Remote caching via Nx Cloud eliminates redundant test runs across team members and CI runs
inputsconfiguration innx.jsondetermines cache invalidation — tune it to avoid false misses- For monorepos with 10+ projects, distributed task execution across CI agents is worth setting up
Nx's affected and caching system doesn't change how you write tests — it changes when they run. Well-structured test suites with fast unit tests and isolated integration tests benefit most from the affected model.