Skip to content

The @nx/conformance/testing package provides utilities for testing conformance rules in a controlled environment.

It's recommended for all conformance rule tests to follow the same pattern using createReadOnlyTree and createStubbedProjectGraphAndFileMapCache when setting up test workspaces:

import type { ReadOnlyConformanceTree } from '@nx/conformance';
import {
applyProjectNodesAndFiles,
createReadOnlyTree,
createStubbedProjectGraphAndFileMapCache,
} from '@nx/conformance/testing';
import type { ProjectGraph } from '@nx/devkit';
import type { FileMapCache } from 'nx/src/project-graph/nx-deps-cache';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import rule from './index';
describe('my-rule', () => {
let tree: ReadOnlyConformanceTree;
let projectGraph: ProjectGraph;
let fileMapCache: FileMapCache;
let cleanup: () => void;
beforeEach(async () => {
// Create the test workspace in a temporary directory and return a read-only tree and a cleanup function
({ tree, cleanup } = await createReadOnlyTree());
// Prepare a stubbed project graph and file map cache for the test workspace
({ projectGraph, fileMapCache } =
await createStubbedProjectGraphAndFileMapCache(tree, [
// Optionally add projects and files to the tree, project graph and file map cache if the rule implementation needs them
// This can also be done later in specific tests using the `applyProjectNodesAndFiles` function
]));
});
// Invoke the cleanup function to remove the generated temporary directory after each test
afterEach(() => cleanup());
it('should return a violation when something specific happens', async () => {
// Optionally add projects and files to the tree, project graph and file map cache if the rule implementation needs them
applyProjectNodesAndFiles(tree, projectGraph, fileMapCache, [
{
projectNode: {
name: 'my-lib',
type: 'lib',
data: {
root: 'libs/my-lib',
},
},
projectFiles: [
// If our specific rule needs to know about project graph dependencies, we add them like so
// In this example this entry causes my-lib to depend on my-app on the project graph and have it correctly attributed to my-lib in the file map cache
{
projectRootRelativeFile: 'src/index.ts',
depsItCreates: ['my-app'],
},
],
},
{
projectNode: {
name: 'my-app',
type: 'app',
data: {
root: 'apps/my-app',
},
},
},
]);
const result = await rule.implementation({
tree,
projectGraph,
fileMapCache,
ruleOptions: {},
});
expect(result.details.violations).toMatchInlineSnapshot(`
// YOUR SNAPSHOT HERE
`);
});
});

You can optionally provide an async callback to createReadOnlyTree to add files or even run Nx generators before the tests run:

const { tree, cleanup } = await createReadOnlyTree(async (writableTree) => {
// Add files to the tree before it becomes read-only
writableTree.write(
'libs/my-lib/custom-config.json',
JSON.stringify({ setting: 'value' })
);
// Run some Nx generator
await libraryGenerator(writableTree, {
// generator options
});
});

If your rule includes a fix generator, you can test it by converting the read-only tree to a writable one using convertToWritable and then running the fix generator:

import { convertToWritable, convertToReadOnly } from '@nx/conformance/testing';
it('should fix violations when fix generator is applied', async () => {
// ... setup and rule implementation test ...
// Convert to a WritableConformanceTree and run fix generator
const writableTree = convertToWritable(tree);
await rule.fixGenerator(writableTree, {
violations: result.details.violations,
ruleOptions: {},
});
const resultAfterFix = await rule.implementation({
// Convert back to a ReadOnlyConformanceTree for the rule implementation and verify the fix worked
tree: convertToReadOnly(writableTree),
projectGraph,
fileMapCache,
ruleOptions: {},
});
expect(resultAfterFix.details.violations).toMatchInlineSnapshot(`[]`);
});

Here's a complete example testing a rule that checks for license headers and includes a fix generator:

import type { ReadOnlyConformanceTree } from '@nx/conformance';
import {
applyProjectNodesAndFiles,
createReadOnlyTree,
createStubbedProjectGraphAndFileMapCache,
convertToWritable,
convertToReadOnly,
} from '@nx/conformance/testing';
import type { ProjectGraph } from '@nx/devkit';
import type { FileMapCache } from 'nx/src/project-graph/nx-deps-cache';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import rule from './license-header-rule';
describe('license-header-rule', () => {
let tree: ReadOnlyConformanceTree;
let projectGraph: ProjectGraph;
let fileMapCache: FileMapCache;
let cleanup: () => void;
beforeEach(async () => {
({ tree, cleanup } = await createReadOnlyTree(async (writableTree) => {
// Create a test file without a license header
writableTree.write(
'libs/my-lib/src/index.ts',
'export const foo = "bar";'
);
}));
({ projectGraph, fileMapCache } =
await createStubbedProjectGraphAndFileMapCache(tree, [
{
projectNode: {
name: 'my-lib',
type: 'lib',
data: {
root: 'libs/my-lib',
},
},
},
]));
});
afterEach(() => cleanup());
it('should detect missing license header', async () => {
const result = await rule.implementation({
tree,
projectGraph,
fileMapCache,
ruleOptions: { addHeader: true },
});
expect(result.details.violations).toHaveLength(1);
expect(result.details.violations[0]).toMatchObject({
message: 'Missing license header',
file: 'libs/my-lib/src/index.ts',
});
});
it('should fix missing license header when fix generator is applied', async () => {
const result = await rule.implementation({
tree,
projectGraph,
fileMapCache,
ruleOptions: { addHeader: true },
});
expect(result.details.violations).toHaveLength(1);
// Apply the fix
const writableTree = convertToWritable(tree);
await rule.fixGenerator(writableTree, {
violations: result.details.violations,
ruleOptions: { addHeader: true },
});
// Verify the fix worked
const resultAfterFix = await rule.implementation({
tree: convertToReadOnly(writableTree),
projectGraph,
fileMapCache,
ruleOptions: { addHeader: true },
});
expect(resultAfterFix.details.violations).toHaveLength(0);
// Verify the file content
const fixedContent = writableTree.read('libs/my-lib/src/index.ts', 'utf-8');
expect(fixedContent).toContain('/* LICENSE */');
});
});
  1. Use snapshots: Use toMatchInlineSnapshot() for violation arrays to easily see changes in test output.
  2. Test isolation: Always use cleanup in the afterEach (or your testing framework equivalent) to ensure tests don't interfere with each other.
  3. Test both detection and fixes: If your rule has a fix generator, test both that violations are detected and that the fix works correctly.
  4. Test edge cases: Test with empty workspaces, missing files, and various project configurations.

Each rule will show its respective execution time and you can use this to identify rules that are slow to run.

Sometimes you may notice that a rule that seems to take a while to complete when run as part of the full rule set but is much faster when run individually. This is because another rule is blocking the main thread. Conformance rules are executed in parallel on the main thread so it is important to avoid blocking actions in a particular rule or it will impact the execution of others.

The @nx/conformance/testing package exports the following utilities:

Creates a read-only tree with a basic Nx workspace structure. Returns Promise<{ tree, cleanup }>.

Parameters:

  • callback (optional): An async function that receives a writable tree before it becomes read-only. Use this to add files or run generators.

Returns:

  • tree: A ReadOnlyConformanceTree instance
  • cleanup: A function to call in afterEach to remove the temporary directory

createStubbedProjectGraphAndFileMapCache(tree, projectNodesWithFiles)

Section titled “createStubbedProjectGraphAndFileMapCache(tree, projectNodesWithFiles)”

Creates a stubbed project graph and file map cache for testing. Returns Promise<{ projectGraph, fileMapCache }>.

Parameters:

  • tree: The ReadOnlyConformanceTree from createReadOnlyTree
  • projectNodesWithFiles: An array of ProjectNodesWithFiles to initialize the graph with (can be empty)

Returns:

  • projectGraph: A stubbed ProjectGraph instance
  • fileMapCache: A stubbed FileMapCache instance

applyProjectNodesAndFiles(tree, projectGraph, fileMapCache, projectNodesWithFiles)

Section titled “applyProjectNodesAndFiles(tree, projectGraph, fileMapCache, projectNodesWithFiles)”

Adds projects and files to the test workspace, updating both the tree and the project graph.

Parameters:

  • tree: The ReadOnlyConformanceTree
  • projectGraph: The ProjectGraph to update
  • fileMapCache: The FileMapCache to update
  • projectNodesWithFiles: An array of ProjectNodesWithFiles describing projects and their files to add

Converts a ReadOnlyConformanceTree to a WritableConformanceTree for fix generator testing.

Parameters:

  • tree: The ReadOnlyConformanceTree to convert

Returns: A WritableConformanceTree instance

Converts a WritableConformanceTree back to a ReadOnlyConformanceTree.

Parameters:

  • tree: The WritableConformanceTree to convert

Returns: A ReadOnlyConformanceTree instance

A type that represents the projects and files to be added to the test workspace:

type ProjectNodesWithFiles = {
projectNode: {
name: string;
type: string;
data: {
root: string;
// ... other project data
};
};
projectFiles?: Array<{
projectRootRelativeFile: string;
depsItCreates?: string[];
}>;
};