Testing in Nx Monorepos: Affected Commands, Task Pipelines, and Caching

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-run

The 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 graph

This 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-cloud

After 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=ci

This 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=1

The 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-agents

DTE 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/merged

Alternatively, 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=test runs 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
  • inputs configuration in nx.json determines 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.

Read more