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:
Run tests for affected packages only:
Package-Specific Tests
Run tests for a specific package:
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:
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();
});
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
});
| Tag | Purpose |
|---|
@mode:postgres | Requires PostgreSQL database |
@mode:queue | Requires queue mode |
@mode:multi-main | Requires multi-main setup |
@licensed | Requires enterprise license |
@capability:email | Requires email server |
@capability:proxy | Requires proxy server |
@db:reset | Reset 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