Node Testing
Testing is crucial for ensuring your nodes work correctly and continue to work as the codebase evolves. n8n uses Jest for unit testing and provides testing utilities for nodes.Testing Setup
All node tests are located in thepackages/nodes-base/nodes/ directory alongside the node files.
File Naming Convention
Copy
Ask AI
MyNode/
├── MyNode.node.ts
├── MyNode.node.test.ts
├── GenericFunctions.ts
└── GenericFunctions.test.ts
Running Tests
Copy
Ask AI
cd packages/nodes-base
pnpm test
Testing Dependencies
Common testing libraries used in n8n:Copy
Ask AI
import { mock } from 'jest-mock-extended';
import type {
IExecuteFunctions,
INodeExecutionData,
ICredentialDataDecryptedObject,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import nock from 'nock';
// Your node
import { MyNode } from './MyNode.node';
| Library | Purpose |
|---|---|
jest | Test framework |
jest-mock-extended | Type-safe mocking |
nock | HTTP mocking |
n8n-workflow | Node interfaces and types |
Basic Test Structure
Here’s a complete test file structure:Copy
Ask AI
import { mock } from 'jest-mock-extended';
import type {
IExecuteFunctions,
INodeExecutionData,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { MyNode } from './MyNode.node';
describe('MyNode', () => {
// Mock execution functions
const executeFunctions = mock<IExecuteFunctions>({
getNode: jest.fn().mockReturnValue({ name: 'MyNode Test' }),
continueOnFail: jest.fn().mockReturnValue(false),
});
beforeEach(() => {
jest.clearAllMocks();
});
describe('execute', () => {
it('should execute successfully', async () => {
// Setup
executeFunctions.getInputData.mockReturnValue([
{ json: { test: 'data' } },
]);
// Execute
const result = await new MyNode().execute.call(executeFunctions);
// Assert
expect(result).toEqual([
[{ json: { success: true }, pairedItems: { item: 0 } }],
]);
});
});
});
Mocking Execution Context
TheIExecuteFunctions interface provides the context for node execution. Mock it appropriately:
- Basic Mocking
- Parameters
- Credentials
- Helpers
Copy
Ask AI
const executeFunctions = mock<IExecuteFunctions>({
getNode: jest.fn().mockReturnValue({
name: 'Test Node',
type: 'n8n-nodes-base.myNode',
}),
continueOnFail: jest.fn().mockReturnValue(false),
getInputData: jest.fn().mockReturnValue([
{ json: { id: 1, name: 'Test' } },
]),
});
Copy
Ask AI
// Mock getNodeParameter for different parameters
executeFunctions.getNodeParameter
.calledWith('operation', 0)
.mockReturnValue('create');
executeFunctions.getNodeParameter
.calledWith('resource', 0)
.mockReturnValue('user');
executeFunctions.getNodeParameter
.calledWith('name', 0)
.mockReturnValue('John Doe');
executeFunctions.getNodeParameter
.calledWith('options', 0)
.mockReturnValue({ timeout: 5000 });
Copy
Ask AI
const credentials = mock<ICredentialDataDecryptedObject>({
apiKey: 'test-api-key',
baseUrl: 'https://api.example.com',
});
executeFunctions.getCredentials
.calledWith('myServiceApi')
.mockResolvedValue(credentials);
Copy
Ask AI
// Mock HTTP request helper
executeFunctions.helpers.request.mockResolvedValue({
id: 1,
name: 'Test User',
});
// Mock returnJsonArray
executeFunctions.helpers.returnJsonArray.mockImplementation(
(data) => data.map((item) => ({ json: item }))
);
HTTP Mocking with Nock
Usenock to mock external API calls:
Copy
Ask AI
import nock from 'nock';
describe('API Tests', () => {
beforeEach(() => {
nock.cleanAll();
});
it('should fetch users', async () => {
// Mock the API response
nock('https://api.example.com')
.get('/users')
.reply(200, [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' },
]);
// Execute node
const result = await new MyNode().execute.call(executeFunctions);
// Assert
expect(result[0]).toHaveLength(2);
expect(result[0][0].json.name).toBe('User 1');
});
});
Real-World Example: AMQP Tests
Here’s the complete AMQP node test from the source code:Copy
Ask AI
import { mock } from 'jest-mock-extended';
import type {
ICredentialDataDecryptedObject,
IExecuteFunctions,
ICredentialTestFunctions,
ICredentialsDecrypted,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { Amqp } from './Amqp.node';
// Mock the entire rhea module
const mockSender = {
close: jest.fn(),
send: jest.fn().mockReturnValue({ id: 'test-message-id' }),
};
const mockConnection = {
close: jest.fn(),
open_sender: jest.fn().mockReturnValue(mockSender),
options: { reconnect: true },
};
const mockContainer = {
connect: jest.fn().mockReturnValue(mockConnection),
on: jest.fn(),
once: jest.fn(),
};
jest.mock('rhea', () => ({
create_container: jest.fn(() => mockContainer),
}));
describe('AMQP Node', () => {
const credentials = mock<ICredentialDataDecryptedObject>({
hostname: 'localhost',
port: 5672,
username: 'testuser',
password: 'testpass',
transportType: 'tcp',
});
const executeFunctions = mock<IExecuteFunctions>({
getNode: jest.fn().mockReturnValue({ name: 'AMQP Test Node' }),
continueOnFail: jest.fn().mockReturnValue(false),
});
beforeEach(() => {
jest.clearAllMocks();
executeFunctions.getCredentials
.calledWith('amqp')
.mockResolvedValue(credentials);
executeFunctions.getInputData.mockReturnValue([
{ json: { testing: true } },
]);
executeFunctions.getNodeParameter
.calledWith('sink', 0)
.mockReturnValue('test/queue');
executeFunctions.getNodeParameter
.calledWith('headerParametersJson', 0)
.mockReturnValue({});
executeFunctions.getNodeParameter
.calledWith('options', 0)
.mockReturnValue({});
// Setup container event mocking
mockContainer.once.mockImplementation((event: string, callback: any) => {
if (event === 'sendable') {
callback({ sender: mockSender });
}
});
});
it('should throw error when sink is empty', async () => {
executeFunctions.getNodeParameter
.calledWith('sink', 0)
.mockReturnValue('');
await expect(new Amqp().execute.call(executeFunctions)).rejects.toThrow(
new NodeOperationError(
executeFunctions.getNode(),
'Queue or Topic required!'
),
);
});
it('should send message successfully', async () => {
const result = await new Amqp().execute.call(executeFunctions);
expect(result).toEqual([
[{ json: { id: 'test-message-id' }, pairedItems: { item: 0 } }],
]);
expect(executeFunctions.getCredentials).toHaveBeenCalledWith('amqp');
expect(mockContainer.connect).toHaveBeenCalled();
expect(mockConnection.open_sender).toHaveBeenCalledWith('test/queue');
expect(mockSender.send).toHaveBeenCalledWith({
application_properties: {},
body: '{"testing":true}',
});
expect(mockSender.close).toHaveBeenCalled();
expect(mockConnection.close).toHaveBeenCalled();
});
it('should handle multiple input items', async () => {
executeFunctions.getInputData.mockReturnValue([
{ json: { item: 1 } },
{ json: { item: 2 } },
]);
const result = await new Amqp().execute.call(executeFunctions);
expect(result).toEqual([
[
{ json: { id: 'test-message-id' }, pairedItems: { item: 0 } },
{ json: { id: 'test-message-id' }, pairedItems: { item: 1 } },
],
]);
expect(mockSender.send).toHaveBeenCalledTimes(2);
});
it('should continue on fail when configured', async () => {
executeFunctions.continueOnFail.mockReturnValue(true);
executeFunctions.getNodeParameter
.calledWith('sink', 0)
.mockReturnValue('');
const result = await new Amqp().execute.call(executeFunctions);
expect(result).toEqual([
[{ json: { error: 'Queue or Topic required!' }, pairedItems: { item: 0 } }],
]);
});
describe('credential test', () => {
it('should return success for valid credentials', async () => {
const amqp = new Amqp();
const testFunctions = mock<ICredentialTestFunctions>();
mockContainer.on.mockImplementation((event: string, callback: any) => {
if (event === 'connection_open') {
setImmediate(() => callback({}));
}
});
const result = await amqp.methods.credentialTest.amqpConnectionTest.call(
testFunctions,
{
data: credentials,
id: 'test',
name: 'test',
type: 'amqp',
} as ICredentialsDecrypted,
);
expect(result).toEqual({
status: 'OK',
message: 'Connection successful!',
});
});
it('should return error for invalid credentials', async () => {
const amqp = new Amqp();
const testFunctions = mock<ICredentialTestFunctions>();
mockContainer.on.mockImplementation((event: string, callback: any) => {
if (event === 'disconnected') {
setImmediate(() =>
callback({ error: new Error('Authentication failed') })
);
}
});
const result = await amqp.methods.credentialTest.amqpConnectionTest.call(
testFunctions,
{
data: credentials,
id: 'test',
name: 'test',
type: 'amqp',
} as ICredentialsDecrypted,
);
expect(result).toEqual({
status: 'Error',
message: 'Authentication failed',
});
});
});
});
Testing Patterns
Test Operations
Test Happy Path
Test the expected successful execution:
Copy
Ask AI
it('should create user successfully', async () => {
executeFunctions.getNodeParameter
.calledWith('operation', 0)
.mockReturnValue('create');
nock('https://api.example.com')
.post('/users')
.reply(201, { id: 1, name: 'New User' });
const result = await new MyNode().execute.call(executeFunctions);
expect(result[0][0].json).toMatchObject({ id: 1, name: 'New User' });
});
Test Error Handling
Test how the node handles errors:
Copy
Ask AI
it('should throw error on API failure', async () => {
nock('https://api.example.com')
.post('/users')
.reply(400, { error: 'Invalid data' });
await expect(
new MyNode().execute.call(executeFunctions)
).rejects.toThrow();
});
Test Edge Cases
Test boundary conditions:
Copy
Ask AI
it('should handle empty input', async () => {
executeFunctions.getInputData.mockReturnValue([]);
const result = await new MyNode().execute.call(executeFunctions);
expect(result).toEqual([[]]);
});
it('should handle missing parameters', async () => {
executeFunctions.getNodeParameter
.calledWith('userId', 0)
.mockReturnValue('');
await expect(
new MyNode().execute.call(executeFunctions)
).rejects.toThrow('User ID is required');
});
Test Continue on Fail
Test error handling with continueOnFail:
Copy
Ask AI
it('should continue on fail when configured', async () => {
executeFunctions.continueOnFail.mockReturnValue(true);
nock('https://api.example.com')
.post('/users')
.reply(400, { error: 'Bad request' });
const result = await new MyNode().execute.call(executeFunctions);
expect(result[0][0].json).toMatchObject({
error: expect.stringContaining('Bad request'),
});
});
Test Binary Data
Copy
Ask AI
it('should handle binary data', async () => {
executeFunctions.getInputData.mockReturnValue([
{
json: {},
binary: {
data: {
data: Buffer.from('test data'),
mimeType: 'text/plain',
fileName: 'test.txt',
},
},
},
]);
const result = await new MyNode().execute.call(executeFunctions);
expect(result[0][0].binary).toBeDefined();
});
Test Pagination
Copy
Ask AI
it('should handle pagination', async () => {
// First page
nock('https://api.example.com')
.get('/users')
.query({ page: 1 })
.reply(200, {
data: [{ id: 1 }, { id: 2 }],
nextPage: 2,
});
// Second page
nock('https://api.example.com')
.get('/users')
.query({ page: 2 })
.reply(200, {
data: [{ id: 3 }, { id: 4 }],
nextPage: null,
});
executeFunctions.getNodeParameter
.calledWith('returnAll', 0)
.mockReturnValue(true);
const result = await new MyNode().execute.call(executeFunctions);
expect(result[0]).toHaveLength(4);
});
Testing LoadOptions
Copy
Ask AI
import type { ILoadOptionsFunctions } from 'n8n-workflow';
describe('loadOptions', () => {
const loadOptionsFunctions = mock<ILoadOptionsFunctions>();
beforeEach(() => {
jest.clearAllMocks();
loadOptionsFunctions.getCredentials
.calledWith('myServiceApi')
.mockResolvedValue({ apiKey: 'test-key' });
});
it('should load users', async () => {
nock('https://api.example.com')
.get('/users')
.reply(200, [
{ id: '1', name: 'User 1' },
{ id: '2', name: 'User 2' },
]);
const node = new MyNode();
const result = await node.methods.loadOptions.getUsers.call(
loadOptionsFunctions,
);
expect(result).toEqual([
{ name: 'User 1', value: '1' },
{ name: 'User 2', value: '2' },
]);
});
});
Testing Credential Tests
Copy
Ask AI
import type { ICredentialTestFunctions } from 'n8n-workflow';
describe('credentialTest', () => {
const credentialTestFunctions = mock<ICredentialTestFunctions>();
it('should validate correct credentials', async () => {
nock('https://api.example.com')
.get('/auth/verify')
.reply(200, { status: 'ok' });
const node = new MyNode();
const result = await node.methods.credentialTest.testApiCredentials.call(
credentialTestFunctions,
{
data: { apiKey: 'valid-key' },
id: 'test',
name: 'test',
type: 'myServiceApi',
} as ICredentialsDecrypted,
);
expect(result).toEqual({
status: 'OK',
message: 'Authentication successful',
});
});
it('should reject invalid credentials', async () => {
nock('https://api.example.com')
.get('/auth/verify')
.reply(401);
const node = new MyNode();
const result = await node.methods.credentialTest.testApiCredentials.call(
credentialTestFunctions,
{
data: { apiKey: 'invalid-key' },
id: 'test',
name: 'test',
type: 'myServiceApi',
} as ICredentialsDecrypted,
);
expect(result.status).toBe('Error');
});
});
Best Practices
Follow these testing best practices for maintainable tests.
DO:
- ✅ Mock all external dependencies
- ✅ Test happy paths, error cases, and edge cases
- ✅ Use
jest.clearAllMocks()inbeforeEach() - ✅ Use
nock.cleanAll()when testing HTTP requests - ✅ Test credential validation
- ✅ Test continue-on-fail behavior
- ✅ Use descriptive test names
- ✅ Test with multiple items
- ✅ Test binary data handling
DON’T:
- ❌ Make real API calls in tests
- ❌ Use
anytype in test code - ❌ Share state between tests
- ❌ Test implementation details
- ❌ Skip error case testing
- ❌ Hardcode test data in multiple places
Coverage Requirements
Aim for high test coverage:- Statements: > 80%
- Branches: > 75%
- Functions: > 80%
- Lines: > 80%
Copy
Ask AI
cd packages/nodes-base
pnpm test -- --coverage
Next Steps
Versioning
Learn how to version your nodes
Node Structure
Review node structure and implementation