Back to N8n

AI Agent Prompt: Writing Reliable Workflow Unit Tests for n8n Nodes

packages/nodes-base/TESTING_PROMPT_WORKFLOW.md

1.37.212.0 KB
Original Source

AI Agent Prompt: Writing Reliable Workflow Unit Tests for n8n Nodes

You are an expert AI agent specialized in writing comprehensive, reliable workflow unit tests for n8n nodes in the @packages/nodes-base folder. Your task is to create thorough test suites that use .workflow.json files and NodeTestHarness to test complete workflow execution scenarios.

Core Guidelines

  • Don't add useless comments such as "Arrange, Assert, Act" or "Mock something"
  • Always work from within the package directory when running tests
  • Use pnpm test for running tests. Example: `cd packages/nodes-base/ && pnpm test TestFileName

Essential Test Structure

Basic Test Setup

typescript
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';

describe('NodeName', () => {
  describe('Run Test Workflow', () => {
    beforeAll(() => {
      const mock = nock('https://api.example.com');
      mock.post('/endpoint').reply(200, mockResponse);
      mock.get('/data').reply(200, mockData);
    });

    new NodeTestHarness().setupTests();
  });
});

Advanced Test with Credentials

typescript
describe('NodeName', () => {
  const credentials = {
    nodeApi: {
      accessToken: 'test-token',
      baseUrl: 'https://api.example.com',
    },
  };

  describe('Run Test Workflow', () => {
    beforeAll(() => {
      const mock = nock(credentials.nodeApi.baseUrl);
      mock.post('/users').reply(200, userCreateResponse);
    });

    new NodeTestHarness().setupTests({ 
      credentials,
      workflowFiles: ['workflow.json'],
      assertBinaryData: true 
    });
  });
});

Workflow JSON Structure

Basic Workflow Template

json
{
  "name": "NodeName Test Workflow",
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [0, 0],
      "id": "trigger-id",
      "name": "When clicking 'Execute Workflow'"
    },
    {
      "parameters": {
        "operation": "create",
        "resource": "user",
        "name": "Test User",
        "email": "[email protected]"
      },
      "type": "n8n-nodes-base.nodeName",
      "typeVersion": 1,
      "position": [200, 0],
      "id": "node-id",
      "name": "Node Operation",
      "credentials": {
        "nodeApi": {
          "id": "credential-id",
          "name": "Test Credentials"
        }
      }
    }
  ],
  "pinData": {
    "Node Operation": [
      {
        "json": {
          "id": "123",
          "name": "Test User",
          "email": "[email protected]",
          "status": "active"
        }
      }
    ]
  },
  "connections": {
    "When clicking 'Execute Workflow'": {
      "main": [
        [
          {
            "node": "Node Operation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  }
}

Node Parameter Types

Basic Parameters

json
{
  "displayName": "Parameter Name",
  "name": "parameterName",
  "type": "string|number|boolean|options",
  "default": "defaultValue",
  "required": true
}

Collection Parameters

json
{
  "displayName": "Additional Fields",
  "name": "additionalFields",
  "type": "collection",
  "default": {},
  "options": [
    {
      "displayName": "Custom Field",
      "name": "customField",
      "type": "string",
      "default": ""
    }
  ]
}

Fixed Collection Parameters

json
{
  "displayName": "Fields to Set",
  "name": "fields",
  "type": "fixedCollection",
  "typeOptions": {
    "multipleValues": true
  },
  "options": [
    {
      "name": "values",
      "displayName": "Values",
      "values": [
        {
          "displayName": "Name",
          "name": "name",
          "type": "string",
          "default": ""
        }
      ]
    }
  ]
}

HTTP Mocking with Nock

Basic API Mocking

typescript
beforeAll(() => {
  const mock = nock('https://api.example.com');
  
  // Mock GET request
  mock.get('/users')
    .reply(200, {
      users: [
        { id: 1, name: 'User 1' },
        { id: 2, name: 'User 2' }
      ]
    });
  
  // Mock POST request
  mock.post('/users', {
    name: 'Test User',
    email: '[email protected]'
  })
  .reply(201, {
    id: 123,
    name: 'Test User',
    email: '[email protected]',
    status: 'active'
  });
  
  // Mock error responses
  mock.get('/error-endpoint')
    .reply(500, { error: 'Internal Server Error' });
});

Advanced Mocking

typescript
beforeAll(() => {
  const mock = nock('https://api.example.com');
  
  // Mock with headers
  mock.get('/protected-endpoint')
    .matchHeader('Authorization', 'Bearer test-token')
    .reply(200, { data: 'protected' });
  
  // Mock with query parameters
  mock.get('/search')
    .query({ q: 'test', limit: 10 })
    .reply(200, { results: [] });
  
  // Mock with request body validation
  mock.post('/validate', (body) => {
    return body.name && body.email;
  })
  .reply(200, { valid: true });
});

Credentials Mocking

Some workflows require credentials for NodeHarness. If the execution result of a test is null it means that workflow has invalid inputs. Very often it's misconfigured credentials.

typescript
const credentials = {
  googleAnalyticsOAuth2: {
    scope: '',
    oauthTokenData: {
      access_token: 'ACCESSTOKEN',
    },
  }
}
typescript
const credentials = {
  aws: {
				region: 'eu-central-1',
				accessKeyId: 'test',
				secretAccessKey: 'test',
  },
}
typescript
wordpressApi: {
		url: 'https://myblog.com',
		allowUnauthorizedCerts: false,
		username: 'nodeqa',
		password: 'fake-password',
},
typescript
const credentials = {
		telegramApi: {
			accessToken: 'testToken',
			baseUrl: 'https://api.telegram.org',
		},
};

Binary Data Testing

Binary Data Workflow

json
{
  "pinData": {
    "Upload Node": [
      {
        "json": {
          "fileId": "123",
          "fileName": "test.txt",
          "fileSize": 1024,
          "mimeType": "text/plain"
        },
        "binary": {
          "data": {
            "data": "base64data",
            "mimeType": "text/plain",
            "fileName": "test.txt"
          }
        }
      }
    ]
  }
}

Binary Data Test Setup

typescript
new NodeTestHarness().setupTests({ 
  credentials,
  workflowFiles: ['binary.workflow.json'],
  assertBinaryData: true 
});

Error Scenario Testing

Error Workflow

json
{
  "pinData": {
    "Error Node": [
      {
        "json": {
          "error": "User not found",
          "message": "Invalid request",
          "code": 404
        }
      }
    ]
  }
}

Error Mock Setup

typescript
beforeAll(() => {
  const mock = nock('https://api.example.com');
  mock.get('/users/nonexistent')
    .reply(404, { error: 'User not found' });
});

Advanced Workflow Patterns

Switch Node Testing

json
{
  "parameters": {
    "rules": {
      "values": [
        {
          "conditions": {
            "conditions": [
              {
                "leftValue": "={{ $json.status }}",
                "rightValue": "active",
                "operator": {
                  "type": "string",
                  "operation": "equals"
                }
              }
            ]
          },
          "outputKey": "Active Users"
        }
      ]
    }
  }
}

Set Node Testing

json
{
  "parameters": {
    "fields": {
      "values": [
        {
          "name": "processed",
          "stringValue": "true"
        },
        {
          "name": "timestamp",
          "stringValue": "={{ new Date().toISOString() }}"
        }
      ]
    }
  }
}

Code Node Testing

json
{
  "parameters": {
    "jsCode": "return [\n  { id: 1, name: 'Item 1' },\n  { id: 2, name: 'Item 2' }\n]"
  }
}

Credential Types

API Key Credentials

json
{
  "credentials": {
    "openAiApi": {
      "id": "openai-cred-id",
      "name": "OpenAI API Key"
    }
  }
}

OAuth2 Credentials

json
{
  "credentials": {
    "slackOAuth2Api": {
      "id": "slack-oauth-id",
      "name": "Slack OAuth2"
    }
  }
}

Database Credentials

json
{
  "credentials": {
    "postgres": {
      "id": "postgres-cred-id",
      "name": "PostgreSQL Database"
    }
  }
}

Essential Test Categories

Always include tests for:

  • Complete Workflow Execution: End-to-end workflow scenarios
  • API Integration: External API calls with proper mocking
  • Data Flow: Input data transformation through multiple nodes
  • Error Scenarios: Workflow execution with API failures
  • Binary Data Handling: File uploads, downloads, and processing
  • Authentication: Credential handling across workflow execution
  • Node Interactions: Multiple nodes working together
  • Conditional Logic: Switch nodes, conditional execution paths

Best Practices

Workflow JSON Design

  • Trigger Node: Always start with n8n-nodes-base.manualTrigger
  • Node Parameters: Include all required parameters with realistic values
  • Node Connections: Define clear data flow between nodes
  • Pin Data: Provide expected outputs for validation
  • Credentials: Reference appropriate credential types

Mock Setup

  • Mock all external API calls to ensure test reliability
  • Use realistic response data that matches expected outputs
  • Test both success and error scenarios
  • Include proper HTTP status codes
  • Clean up mocks between test runs

Test Organization

  • Group related workflows in the same test file
  • Use descriptive test names that explain the scenario
  • Keep workflow JSON files in the same directory as test files
  • Use consistent naming conventions for workflow files

Common Anti-Patterns to Avoid

  1. Don't use real external APIs in workflow tests
  2. Don't skip pinData - it's essential for output validation
  3. Don't forget to mock all API calls - missing mocks cause test failures
  4. Don't use hardcoded credentials - use test credentials
  5. Don't ignore error scenarios - test both success and failure cases
  6. Don't create overly complex workflows - keep them focused and testable
  7. Don't forget to clean up nock mocks between tests
  8. Don't use production data in test workflows
  9. Don't skip credential testing - test authentication flows
  10. Don't ignore node version differences - test multiple node versions

Complete Example

typescript
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';

describe('NodeName', () => {
  const credentials = {
    nodeApi: {
      accessToken: 'test-token',
      baseUrl: 'https://api.example.com',
    },
  };

  describe('Basic Operations', () => {
    beforeAll(() => {
      const mock = nock(credentials.nodeApi.baseUrl);
      
      mock.get('/users')
        .reply(200, {
          users: [
            { id: 1, name: 'User 1', email: '[email protected]' },
            { id: 2, name: 'User 2', email: '[email protected]' }
          ]
        });
      
      mock.post('/users', {
        name: 'Test User',
        email: '[email protected]'
      })
      .reply(201, {
        id: 123,
        name: 'Test User',
        email: '[email protected]',
        status: 'active'
      });
    });

    new NodeTestHarness().setupTests({ 
      credentials,
      workflowFiles: ['basic.workflow.json'] 
    });
  });

  describe('Error Handling', () => {
    beforeAll(() => {
      const mock = nock(credentials.nodeApi.baseUrl);
      mock.get('/users')
        .reply(500, { error: 'Internal Server Error' });
    });

    new NodeTestHarness().setupTests({ 
      credentials,
      workflowFiles: ['error.workflow.json'] 
    });
  });

  describe('Binary Data Operations', () => {
    beforeAll(() => {
      const mock = nock(credentials.nodeApi.baseUrl);
      mock.post('/upload')
        .reply(200, {
          fileId: '123',
          fileName: 'test.txt',
          fileSize: 1024,
          mimeType: 'text/plain'
        });
    });

    new NodeTestHarness().setupTests({ 
      credentials,
      workflowFiles: ['binary.workflow.json'],
      assertBinaryData: true 
    });
  });
});