Skip to main content

Testing Overview

n8n employs a comprehensive testing strategy with multiple layers:
  • Unit Tests: Jest for backend and nodes, Vitest for frontend
  • Integration Tests: Jest with mocked dependencies
  • E2E Tests: Playwright for full UI and API testing
  • Workflow Tests: JSON-based integration tests for nodes

Running Tests

All Tests

Run all tests across the monorepo:
pnpm test
Run tests for affected packages only:
pnpm test:affected

Package-Specific Tests

Run tests for a specific package:
1

Navigate to the package

cd packages/cli
2

Run the tests

pnpm test
Always run tests from within the package directory. Running pnpm test from the monorepo root runs all tests, which can be time-consuming.

Specific Test Files

Run a specific test file:
cd packages/cli
pnpm test src/services/workflow.service.test.ts
Run tests matching a pattern:
pnpm test -- --testPathPattern=workflow

Unit Testing with Jest

Backend Unit Tests

n8n uses Jest for backend unit tests with the following conventions:
// packages/cli/src/services/workflow.service.test.ts
import { WorkflowService } from './workflow.service';
import { WorkflowRepository } from '../repositories/workflow.repository';
import { mock } from 'jest-mock-extended';

describe('WorkflowService', () => {
  let workflowService: WorkflowService;
  let workflowRepository: jest.Mocked<WorkflowRepository>;

  beforeEach(() => {
    workflowRepository = mock<WorkflowRepository>();
    workflowService = new WorkflowService(workflowRepository);
  });

  describe('findById', () => {
    it('should return workflow when found', async () => {
      const mockWorkflow = { id: '1', name: 'Test' };
      workflowRepository.findById.mockResolvedValue(mockWorkflow);

      const result = await workflowService.findById('1');

      expect(result).toEqual(mockWorkflow);
      expect(workflowRepository.findById).toHaveBeenCalledWith('1');
    });

    it('should throw error when workflow not found', async () => {
      workflowRepository.findById.mockResolvedValue(null);

      await expect(workflowService.findById('1'))
        .rejects.toThrow('Workflow not found');
    });
  });
});

Frontend Unit Tests

Frontend tests use Vitest:
// packages/frontend/editor-ui/src/stores/workflows.store.test.ts
import { setActivePinia, createPinia } from 'pinia';
import { useWorkflowsStore } from './workflows.store';
import { vi } from 'vitest';

describe('Workflows Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia());
  });

  it('should fetch workflows', async () => {
    const store = useWorkflowsStore();
    const mockWorkflows = [{ id: '1', name: 'Test' }];

    // Mock API call
    vi.spyOn(store, 'fetchWorkflows').mockResolvedValue(mockWorkflows);

    await store.fetchWorkflows();

    expect(store.workflows).toEqual(mockWorkflows);
  });
});

Snapshot Updates

If tests fail due to expected changes, update snapshots:
pnpm test -- -u
Or press u in Jest watch mode.

Code Coverage

n8n tracks code coverage on Codecov.

Local Coverage

Generate coverage reports locally:
COVERAGE_ENABLED=true pnpm test
View coverage in the coverage/ folder or use the VSCode Coverage Gutters extension.

E2E Testing with Playwright

n8n uses Playwright for comprehensive E2E testing.

Running E2E Tests

# Run tests against local server
pnpm --filter=n8n-playwright test:local

Test Architecture

Playwright tests follow a layered architecture:
Tests (*.spec.ts)
    ↓ uses
Composables (*Composer.ts) - Business workflows
    ↓ orchestrates
Page Objects (*Page.ts) - UI interactions
    ↓ extends
BasePage - Common utilities

Page Object Pattern

Page objects encapsulate UI interactions:
// packages/testing/playwright/pages/WorkflowsPage.ts
import { BasePage } from './BasePage';
import type { Locator } from '@playwright/test';

export class WorkflowsPage extends BasePage {
  // Getters: Return Locator, no async
  getWorkflowCards(): Locator {
    return this.page.getByTestId('workflow-card');
  }

  getWorkflowByName(name: string): Locator {
    return this.getWorkflowCards().filter({ hasText: name });
  }

  // Actions: Async, descriptive verb, returns void
  async clickAddWorkflowButton(): Promise<void> {
    await this.page.getByTestId('add-workflow').click();
  }

  async searchWorkflows(searchTerm: string): Promise<void> {
    await this.page.getByTestId('search-input').fill(searchTerm);
  }

  // Queries: Async, returns data
  async getWorkflowCount(): Promise<number> {
    return await this.getWorkflowCards().count();
  }
}

Composables

Composables orchestrate multi-step business workflows:
// packages/testing/playwright/composables/WorkflowComposer.ts
export class WorkflowComposer {
  constructor(private n8n: N8nFixture) {}

  async createWorkflow(name?: string) {
    await this.n8n.workflows.clickAddWorkflowButton();
    const workflowName = name ?? `Workflow ${Date.now()}`;
    await this.n8n.canvas.setWorkflowName(workflowName);
    await this.n8n.canvas.saveWorkflow();
    return workflowName;
  }

  async executeWorkflowAndWaitForSuccess() {
    const responsePromise = this.n8n.page.waitForResponse(
      (response) => response.url().includes('/run') &&
                    response.request().method() === 'POST'
    );
    await this.n8n.canvas.clickExecuteButton();
    await responsePromise;
    await this.n8n.notifications.waitForNotificationAndClose('Successful');
  }
}

Writing E2E Tests

// packages/testing/playwright/tests/workflows/create.spec.ts
import { test, expect } from '../fixtures/base';

test('should create a new workflow', async ({ n8n }) => {
  // Use entry point
  await n8n.start.fromHome();

  // Use composables for complex workflows
  const workflowName = await n8n.workflowComposer.createWorkflow();

  // Use page objects for assertions
  await expect(n8n.workflows.getWorkflowByName(workflowName)).toBeVisible();
});

test('should execute workflow successfully', async ({ n8n }) => {
  await n8n.start.fromBlankCanvas();

  // Use page objects for simple interactions
  await n8n.canvas.addNode('Manual Trigger');
  await n8n.canvas.addNode('Set');

  // Use composables for complex flows
  await n8n.workflowComposer.executeWorkflowAndWaitForSuccess();
});

Test Tags

Use tags to control test execution:
// Basic test - runs in all modes
test('basic workflow creation', async ({ n8n }) => {
  // Test code
});

// PostgreSQL only
test('postgres-specific feature @mode:postgres', async ({ n8n }) => {
  // Test code
});

// Enterprise features (container-only)
test('log streaming @licensed', async ({ n8n }) => {
  // Test code
});

// Requires specific capability
test('email notification @capability:email', async ({ n8n }) => {
  // Test code
});
TagPurpose
@mode:postgresRequires PostgreSQL database
@mode:queueRequires queue mode
@mode:multi-mainRequires multi-main setup
@licensedRequires enterprise license
@capability:emailRequires email server
@capability:proxyRequires proxy server
@db:resetReset database before each test

Test Isolation

For tests requiring isolated databases:
import { test, expect } from '../fixtures/base';

// Unique capability value gives fresh container
test.use({ capability: { env: { TEST_ISOLATION: 'my-test-suite' } } });

test.describe('Isolated tests', () => {
  test.describe.configure({ mode: 'serial' }); // If tests depend on each other

  test('test with clean state', async ({ n8n }) => {
    // Fresh container with reset database
  });
});
For per-test database reset:
test.use({ capability: { env: { TEST_ISOLATION: 'stateful-tests' } } });

test.describe('Stateful tests @db:reset', () => {
  test('test 1', async ({ n8n }) => {
    // Fresh database before this test
  });

  test('test 2', async ({ n8n }) => {
    // Fresh database again
  });
});
Tests with @db:reset only run in container mode, not locally.

Node Workflow Tests

Nodes include JSON-based workflow tests:
// packages/nodes-base/nodes/Switch/V3/test/node/workflow.json
{
  "nodes": [
    {
      "name": "When clicking 'Execute workflow'",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [250, 300]
    },
    {
      "name": "Switch",
      "type": "n8n-nodes-base.switch",
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "number": [
                  {
                    "value1": "={{ $json.value }}",
                    "operation": "larger",
                    "value2": 5
                  }
                ]
              }
            }
          ]
        }
      },
      "position": [450, 300]
    }
  ],
  "connections": {
    "When clicking 'Execute workflow'": {
      "main": [[{ "node": "Switch", "type": "main", "index": 0 }]]
    }
  }
}
Tests verify node behavior with real workflow execution.

Mocking Dependencies

Server Mocking with nock

Backend tests use nock for HTTP mocking:
import nock from 'nock';

test('should fetch external API', async () => {
  nock('https://api.example.com')
    .get('/data')
    .reply(200, { result: 'success' });

  const result = await fetchData();
  expect(result).toEqual({ result: 'success' });
});

Playwright Proxy Server

E2E tests can use proxy server for API mocking:
import { test, expect } from '../fixtures/base';

test.describe('Proxy tests @capability:proxy', () => {
  test('should mock HTTP requests', async ({ proxyServer, n8n }) => {
    // Create mock expectation
    await proxyServer.createGetExpectation('/api/data', { result: 'mocked' });

    // Execute workflow that makes HTTP requests
    await n8n.canvas.openNewWorkflow();
    await n8n.canvas.addNode('HTTP Request');
    await n8n.ndv.fillParameterInput('URL', 'https://api.example.com/data');
    await n8n.workflowComposer.executeWorkflowAndWaitForSuccess();

    // Verify request was proxied
    expect(await proxyServer.wasGetRequestMade('/api/data')).toBe(true);
  });
});

Testing Best Practices

Mock all external dependencies in unit tests. Never make real HTTP requests or database calls.
Confirm test cases with the team before writing complex unit tests for new features.
Always run typecheck before committing:
cd packages/cli
pnpm typecheck
Use unique identifiers in E2E tests to avoid conflicts:
import { nanoid } from 'nanoid';

const workflowName = `Test Workflow ${nanoid()}`;
Never use waitForTimeout - use proper Playwright waiting strategies:
// Bad
await page.waitForTimeout(1000);

// Good
await page.waitForResponse('**/api/workflows/**');
await expect(element).toBeVisible();

Debugging Tests

Playwright Debugging

test('debug test', async ({ n8n }) => {
  await n8n.page.pause(); // Opens Playwright inspector
});

Jest Debugging

# Run specific test in debug mode
node --inspect-brk node_modules/.bin/jest --runInBand src/services/workflow.service.test.ts
Then attach your debugger (VS Code, Chrome DevTools, etc.)

CI/CD Testing

Tests run automatically on CI:
# Backend tests
pnpm test:ci:backend

# Frontend tests
pnpm test:ci:frontend

# E2E tests
pnpm test:with:docker

Test Maintenance

The janitor tool helps maintain test quality:
# Run static analysis
npx tsx scripts/janitor/index.ts

# Find dead code
npx tsx scripts/janitor/index.ts --rule=dead-code

# Auto-fix issues
npx tsx scripts/janitor/index.ts --rule=dead-code --fix --write
See packages/testing/playwright/README.md for more details.

Next Steps