| name | node-red-testing |
| description | Comprehensive guide for testing Node-RED nodes. This skill should be used when writing unit tests with mocks, integration tests with Docker/testcontainers, end-to-end tests against Node-RED runtime, or setting up test infrastructure for Node-RED node packages. |
Node-RED Testing
This skill provides patterns, tools, and strategies for testing custom Node-RED nodes at all levels: unit, integration, and end-to-end.
When to Use This Skill
- Writing unit tests for Node-RED nodes with mocked RED framework
- Setting up integration tests with Docker or testcontainers
- Creating end-to-end tests against a real Node-RED runtime
- Configuring test runners (Jest, Mocha) for Node-RED projects
- Testing message flows, error handling, and node lifecycle
Quick Start
To scaffold test files for a node, run the initialization script:
python scripts/init_test.py my-node --type unit --path ./test
python scripts/init_test.py my-node --type integration --path ./test
python scripts/init_test.py my-node --type e2e --path ./test
python scripts/init_test.py my-node --type all --path ./test
Test Pyramid Overview
/\
/E2E\ <- Few: Full Node-RED runtime tests
/------\
/Integr- \ <- Some: Docker-based, real dependencies
/ ation \
/------------\
/ Unit \ <- Many: Fast, isolated, mocked tests
/________________\
Unit Testing with Mocks
Unit tests verify node logic in isolation by mocking the Node-RED runtime (RED object) and simulating message flows.
Mock RED Framework
Create a mock RED object that simulates the Node-RED runtime:
function createMockRED() {
const nodes = new Map();
const registeredTypes = new Map();
return {
nodes: {
createNode: function(node, config) {
node.id = config.id || 'test-node-id';
node.type = config.type || 'test-node';
node.name = config.name || '';
node._events = {};
node.on = function(event, handler) {
this._events[event] = this._events[event] || [];
this._events[event].push(handler);
};
node.emit = function(event, ...args) {
const handlers = this._events[event] || [];
handlers.forEach(h => h.apply(node, args));
};
node.send = jest.fn();
node. = jest.();
node. = jest.();
node. = jest.();
node. = jest.();
node. = jest.();
node. = jest.();
node. = () {
store = {};
{
: store[key],
: { store[key] = val; },
: {
: store[],
: { store[] = val; }
},
: {
: store[],
: { store[] = val; }
}
};
};
nodes.(node., node);
},
: () {
registeredTypes.(type, { constructor, opts });
},
: () {
nodes.(id);
}
},
: () {
registration = registeredTypes.(type);
(!registration) {
();
}
node = {};
registration..(node, config);
node;
},
: () {
registeredTypes;
}
};
}
. = { createMockRED };
Test Node Registration
const { createMockRED } = require('./helpers/mock-red');
const myNodeModule = require('../nodes/my-node');
describe('my-node registration', () => {
let RED;
beforeEach(() => {
RED = createMockRED();
myNodeModule(RED);
});
it('should register the node type', () => {
const types = RED._getRegisteredTypes();
expect(types.has('my-node')).toBe(true);
});
it('should create node with config', () => {
const config = { id: 'n1', name: 'Test Node', type: 'my-node' };
const node = RED._createTestNode('my-node', config);
expect(node.name).toBe('Test Node');
});
});
Test Message Input Handling
describe('my-node input handling', () => {
let RED, node;
beforeEach(() => {
RED = createMockRED();
myNodeModule(RED);
node = RED._createTestNode('my-node', { id: 'n1' });
});
it('should process input message', () => {
const msg = { payload: 'test data' };
const send = jest.fn();
const done = jest.fn();
node.emit('input', msg, send, done);
expect(send).toHaveBeenCalledWith(expect.objectContaining({
payload: expect.any(String)
}));
expect(done).toHaveBeenCalled();
});
it('should handle errors gracefully', () => {
const msg = { payload: null };
const send = jest.fn();
done = jest.();
node.(, msg, send, done);
(done).(expect.());
});
});
Test Node Status Updates
describe('my-node status', () => {
let RED, node;
beforeEach(() => {
RED = createMockRED();
myNodeModule(RED);
node = RED._createTestNode('my-node', { id: 'n1' });
});
it('should set connecting status on init', () => {
expect(node.status).toHaveBeenCalledWith({
fill: 'yellow',
shape: 'ring',
text: 'connecting'
});
});
it('should set connected status after successful connection', async () => {
await node._connect();
expect(node.status).toHaveBeenCalledWith({
fill: 'green',
shape: 'dot',
text: 'connected'
});
});
});
Test Node Cleanup
describe('my-node cleanup', () => {
let RED, node;
beforeEach(() => {
RED = createMockRED();
myNodeModule(RED);
node = RED._createTestNode('my-node', { id: 'n1' });
});
it('should cleanup on close', (done) => {
node.emit('close', false, () => {
expect(node._connection).toBeNull();
done();
});
});
it('should handle removed flag', (done) => {
node.emit('close', true, () => {
done();
});
});
});
Mock External Dependencies
jest.mock('some-external-lib', () => ({
connect: jest.fn().mockResolvedValue({ connected: true }),
disconnect: jest.fn().mockResolvedValue(undefined)
}));
const externalLib = require('some-external-lib');
describe('my-node with external lib', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should call external lib on init', () => {
RED = createMockRED();
myNodeModule(RED);
node = RED._createTestNode('my-node', { id: 'n1' });
expect(externalLib.connect).toHaveBeenCalled();
});
});
Integration Testing with Docker
Integration tests verify nodes work with real external dependencies using Docker containers.
Testcontainers Setup
For detailed testcontainers configuration and patterns, see references/testcontainers-setup.md.
Basic Docker Compose for Testing
version: '3.8'
services:
node-red:
image: nodered/node-red:latest
ports:
- "1880:1880"
volumes:
- ./test-flows:/data/flows
- ./nodes:/data/node_modules/@myorg/my-nodes
environment:
- NODE_RED_ENABLE_SAFE_MODE=false
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:1880/"]
interval: 5s
timeout: 3s
retries: 10
redis:
image: redis:7-alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 2s
timeout: 2s
retries: 5
mosquitto:
image: eclipse-mosquitto:2
ports:
Integration Test with Docker Compose
const { execSync } = require('child_process');
const axios = require('axios');
const NODE_RED_URL = 'http://localhost:1880';
describe('my-node integration', () => {
beforeAll(async () => {
execSync('docker-compose -f docker-compose.test.yml up -d', {
stdio: 'inherit'
});
await waitForNodeRED();
}, 60000);
afterAll(() => {
execSync('docker-compose -f docker-compose.test.yml down -v', {
stdio: 'inherit'
});
});
it('should process messages through the flow', async () => {
const flow = require('./fixtures/test-flow.json');
await axios.post(`${NODE_RED_URL}/flows`, flow, {
headers: { 'Content-Type': 'application/json' }
});
response = axios.(, {
:
});
(response.).();
(response.).({ : });
});
});
() {
( i = ; i < maxAttempts; i++) {
{
axios.();
;
} (e) {
( (r, ));
}
}
();
}
Health Check Utilities
const { execSync } = require('child_process');
const axios = require('axios');
async function waitForService(url, options = {}) {
const {
maxAttempts = 30,
interval = 2000,
healthPath = '/',
expectedStatus = 200
} = options;
for (let i = 0; i < maxAttempts; i++) {
try {
const response = await axios.get(`${url}${healthPath}`);
if (response.status === expectedStatus) {
return true;
}
} catch (e) {
}
await new Promise(r => setTimeout(r, interval));
}
throw new Error(`Service at ${url} did not become ready`);
}
function startContainers(composeFile = 'docker-compose.test.yml') {
execSync(`docker-compose -f up -d`, { : });
}
() {
(, { : });
}
() {
().();
}
. = {
waitForService,
startContainers,
stopContainers,
getContainerLogs
};
Message Flow Testing
describe('message flow testing', () => {
it('should pass messages through multiple nodes', async () => {
const flow = [
{
id: 'inject1',
type: 'inject',
payload: '{"test": "data"}',
payloadType: 'json',
wires: [['mynode1']]
},
{
id: 'mynode1',
type: 'my-node',
name: 'Test Node',
wires: [['debug1']]
},
{
id: 'debug1',
type: 'debug',
active: true,
console: true
}
];
await deployFlow(flow);
await axios.post(`${NODE_RED_URL}/inject/inject1`);
const logs = getContainerLogs('node-red');
(logs).();
});
});
End-to-End Testing
E2E tests verify the complete system including Node-RED runtime, UI, and external integrations.
Node-RED Runtime Integration
const helper = require('node-red-node-test-helper');
const myNode = require('../nodes/my-node');
helper.init(require.resolve('node-red'));
describe('my-node E2E', () => {
beforeEach((done) => {
helper.startServer(done);
});
afterEach((done) => {
helper.unload();
helper.stopServer(done);
});
it('should load node', (done) => {
const flow = [{ id: 'n1', type: 'my-node', name: 'test' }];
helper.load(myNode, flow, () => {
const n1 = helper.getNode('n1');
expect(n1).toHaveProperty('name', 'test');
done();
});
});
it('should process messages', (done) => {
const flow = [
{ : , : , : , : [[]] },
{ : , : }
];
helper.(myNode, flow, {
n1 = helper.();
n2 = helper.();
n2.(, {
(msg).();
();
});
n1.({ : });
});
});
(, {
flow = [
{ : , : , : , : }
];
helper.(myNode, flow, {
n1 = helper.();
(n1.).();
();
});
});
});
Testing with Configuration Nodes
describe('my-node with config node', () => {
it('should use config node', (done) => {
const flow = [
{ id: 'config1', type: 'my-server', host: 'localhost', port: 8080 },
{ id: 'n1', type: 'my-node', server: 'config1', wires: [['n2']] },
{ id: 'n2', type: 'helper' }
];
helper.load([myNode, myServerNode], flow, () => {
const n1 = helper.getNode('n1');
expect(n1.server).toBeDefined();
expect(n1.server.host).toBe('localhost');
done();
});
});
});
Testing Credentials
describe('my-node credentials', () => {
it('should handle credentials', (done) => {
const flow = [
{ id: 'n1', type: 'my-node', name: 'test' }
];
const credentials = {
n1: { username: 'user', password: 'pass' }
};
helper.load(myNode, flow, credentials, () => {
const n1 = helper.getNode('n1');
expect(n1.credentials.username).toBe('user');
expect(n1.credentials.password).toBe('pass');
done();
});
});
});
HTTP Endpoint Testing
describe('my-node HTTP endpoints', () => {
it('should respond to HTTP requests', (done) => {
const flow = [
{ id: 'n1', type: 'http in', url: '/test', method: 'get', wires: [['n2']] },
{ id: 'n2', type: 'my-node', wires: [['n3']] },
{ id: 'n3', type: 'http response' }
];
helper.load([httpIn, myNode, httpResponse], flow, () => {
helper.request()
.get('/test')
.expect(200)
.end((err, res) => {
if (err) return done(err);
expect(res.body).toMatchObject({ success: true });
done();
});
});
});
});
UI Testing with Playwright
const { test, expect } = require('@playwright/test');
test.describe('my-node editor UI', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:1880');
await page.waitForSelector('.red-ui-palette');
});
test('should appear in palette', async ({ page }) => {
const paletteNode = page.locator('.red-ui-palette-node[data-palette-type="my-node"]');
await expect(paletteNode).toBeVisible();
});
test('should open edit dialog', async ({ page }) => {
const paletteNode = page.locator('.red-ui-palette-node[data-palette-type="my-node"]');
const workspace = page.locator('#red-ui-workspace-chart');
await paletteNode.dragTo(workspace);
const node = page.locator('.red-ui-flow-node').first();
await node.dblclick();
(page.()).();
});
(, ({ page }) => {
page.(, );
page.(, );
page.();
node = page.().();
(node).();
});
});
Test Organization & Patterns
Directory Structure
project/
├── nodes/
│ ├── my-node.js
│ └── my-node.html
├── test/
│ ├── helpers/
│ │ ├── mock-red.js
│ │ └── docker-utils.js
│ ├── fixtures/
│ │ └── test-flow.json
│ ├── unit/
│ │ └── my-node.unit.test.js
│ ├── integration/
│ │ └── my-node.integration.test.js
│ └── e2e/
│ └── my-node.e2e.test.js
├── docker-compose.test.yml
├── jest.config.js
└── package.json
Jest Configuration
module.exports = {
testEnvironment: 'node',
testMatch: [
'**/test/**/*.test.js'
],
collectCoverageFrom: [
'nodes/**/*.js',
'!nodes/**/*.html'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
setupFilesAfterEnv: ['./test/setup.js'],
testTimeout: 30000,
projects: [
{
displayName: 'unit',
testMatch: ['<rootDir>/test/unit/**/*.test.js'],
testTimeout: 5000
},
{
displayName: 'integration',
testMatch: ['<rootDir>/test/integration/**/*.test.js'],
testTimeout: 60000
},
{
displayName: 'e2e',
testMatch: ['<rootDir>/test/e2e/**/*.test.js'],
testTimeout: 120000
}
]
};
Test Setup File
jest.setTimeout(30000);
global.sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
afterAll(async () => {
});
Package.json Scripts
{
"scripts": {
"test": "jest",
"test:unit": "jest --selectProjects unit",
"test:integration": "jest --selectProjects integration",
"test:e2e": "jest --selectProjects e2e",
"test:watch": "jest --watch --selectProjects unit",
"test:coverage": "jest --coverage",
"test:ci": "jest --ci --coverage --runInBand",
"docker:test:up": "docker-compose -f docker-compose.test.yml up -d",
"docker:test:down": "docker-compose -f docker-compose.test.yml down -v"
}
}
Test Isolation Patterns
describe('isolated tests', () => {
let RED, node;
beforeEach(() => {
RED = createMockRED();
jest.clearAllMocks();
});
afterEach(() => {
node = null;
});
});
Fixture Management
const path = require('path');
const fs = require('fs');
function loadFixture(name) {
const fixturePath = path.join(__dirname, `${name}.json`);
return JSON.parse(fs.readFileSync(fixturePath, 'utf8'));
}
function createMessage(overrides = {}) {
return {
_msgid: 'test-msg-id',
payload: 'default payload',
topic: '',
...overrides
};
}
module.exports = { loadFixture, createMessage };
Testing Async Operations
describe('async operations', () => {
it('should handle async input processing', async () => {
const node = RED._createTestNode('my-node', { id: 'n1' });
const result = await new Promise((resolve, reject) => {
const send = jest.fn((msg) => resolve(msg));
const done = jest.fn((err) => err ? reject(err) : null);
node.emit('input', { payload: 'test' }, send, done);
});
expect(result.payload).toBe('processed');
});
it('should timeout long operations', async () => {
jest.useFakeTimers();
const promise = node.longOperation();
jest.advanceTimersByTime(5000);
await expect(promise).rejects.();
jest.();
});
});
Common Testing Mistakes
- Not isolating tests - Always create fresh mocks in
beforeEach
- Forgetting async cleanup - Use
afterEach and afterAll properly
- Hard-coded timeouts - Use configurable wait utilities
- Testing implementation details - Focus on behavior, not internals
- Missing error cases - Test both success and failure paths
- No integration tests - Unit tests alone miss real-world issues
- Flaky Docker tests - Use proper health checks and retries
- Shared state between tests - Reset all state in
beforeEach
Reference Documentation
For detailed setup guides, consult the references directory:
references/testcontainers-setup.md - Docker and testcontainers configuration
To search for specific patterns:
grep -r "pattern" references/