mcpField
N
n8n-expression-testing

n8n-expression-testing

github.com/proffesor-for-testing测试与质量519122 次安装今天更新
代码仓库

关于 n8n-expression-testing

面向开发者的 n8n 表达式测试技能,涵盖语法、上下文变量、空值处理、类型、安全性和性能检查。

主要功能

  1. 01验证表达式语法
  2. 02测试 n8n 上下文变量
  3. 03检查空值安全和类型处理
  4. 04扫描危险函数
  5. 05检查数组操作效率

适用场景

  1. 01发现不安全的嵌套属性访问
  2. 02使用不同样例上下文检查表达式
  3. 03识别有风险或低效的表达式模式

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/proffesor-for-testing/agentic-qe --skill n8n-expression-testing

安装后在 Claude Code 中输入/n8n-expression-testing确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evals/
n8n-expression-testing.yaml
schemas/
output.json
scripts/
validate-config.json

详细介绍

n8n 表达式测试

<default_to_action> 测试 n8n 表达式时:

  1. 执行前验证语法
  2. 使用多种上下文场景进行测试
  3. 检查对 null/undefined 的处理
  4. 验证类型安全性
  5. 扫描安全漏洞

快速表达式检查清单:

  • JavaScript 语法有效
  • 正确引用上下文变量($json、$node)
  • 使用空值安全访问模式(?.、??)
  • 不包含危险函数(eval、Function)
  • 对大型数据集而言效率高

常见陷阱:

  • 未进行空值检查就访问嵌套属性
  • 类型强制转换问题
  • 缺少回退值
  • 数组操作效率低下 </default_to_action>

快速参考卡

n8n 表达式语法
模式示例描述
基本访问{{ $json.field }}访问 JSON 字段
嵌套访问{{ $json.user.email }}访问嵌套属性
数组访问{{ $json.items[0] }}访问数组元素
节点引用{{ $node["Name"].json.id }}访问其他节点的数据
方法调用{{ $json.name.toLowerCase() }}调用字符串方法
条件表达式{{ $json.x ? "yes" : "no" }}三元表达式
上下文变量
变量描述示例
$json当前项数据{{ $json.email }}
$node["Name"]其他节点的数据{{ $node["HTTP"].json.body }}
$items()多个项{{ $items("Node", 0, 0).json }}
$now当前时间戳{{ $now.toISO() }}
$today今天的日期{{ $today }}
$runIndex运行迭代{{ $runIndex }}
$workflow工作流信息{{ $workflow.name }}

表达式语法模式

安全数据访问
// BAD: Can fail if nested objects are null
{{ $json.user.profile.email }}

// GOOD: Optional chaining with fallback
{{ $json.user?.profile?.email ?? '' }}

// BAD: Array access without bounds check
{{ $json.items[0].name }}

// GOOD: Safe array access
{{ $json.items?.[0]?.name ?? 'No items' }}
类型转换
// String to Number
{{ parseInt($json.quantity, 10) }}
{{ parseFloat($json.price) }}
{{ Number($json.value) }}

// Number to String
{{ String($json.id) }}
{{ $json.amount.toString() }}
{{ $json.count.toFixed(2) }}

// Date handling
{{ new Date($json.timestamp).toISOString() }}
{{ DateTime.fromISO($json.date).toFormat('yyyy-MM-dd') }}

// Boolean conversion
{{ Boolean($json.active) }}
{{ $json.enabled === 'true' }}
字符串操作
// Case conversion
{{ $json.name.toLowerCase() }}
{{ $json.name.toUpperCase() }}
{{ $json.name.charAt(0).toUpperCase() + $json.name.slice(1) }}

// String manipulation
{{ $json.text.trim() }}
{{ $json.text.replace(/\s+/g, ' ') }}
{{ $json.text.substring(0, 100) }}

// Template strings
{{ `Hello, ${$json.firstName} ${$json.lastName}!` }}
{{ `Order #${$json.orderId} - ${$json.status}` }}
数组操作
// Mapping
{{ $json.items.map(item => item.name) }}
{{ $json.items.map(item => ({ id: item.id, total: item.price * item.qty })) }}

// Filtering
{{ $json.items.filter(item => item.active) }}
{{ $json.items.filter(item => item.price > 100) }}

// Reducing
{{ $json.items.reduce((sum, item) => sum + item.price, 0) }}
{{ $json.items.reduce((acc, item) => ({ ...acc, [item.id]: item }), {}) }}

// Finding
{{ $json.items.find(item => item.id === $json.targetId) }}
{{ $json.items.findIndex(item => item.name === 'target') }}

// Joining
{{ $json.tags.join(', ') }}
{{ $json.items.map(i => i.name).join(' | ') }}

验证模式

// Validate expression syntax
function validateExpressionSyntax(expression: string): ValidationResult {
  // Remove n8n template markers
  const code = expression.replace(/\{\{|\}\}/g, '').trim();

  try {
    // Check if valid JavaScript
    new Function(`return (${code})`);
    return { valid: true };
  } catch (error) {
    return {
      valid: false,
      error: error.message,
      suggestion: suggestFix(error.message, code)
    };
  }
}

// Validate context variables
function validateContextVariables(expression: string): string[] {
  const contextVars = ['$json', '$node', '$items', '$now', '$today', '$runIndex', '$workflow'];
  const usedVars = [];
  const invalidVars = [];

  // Find all $ prefixed variables
  const varPattern = /\$\w+/g;
  let match;

  while ((match = varPattern.exec(expression)) !== null) {
    const varName = match[0];
    if (contextVars.some(cv => varName.startsWith(cv))) {
      usedVars.push(varName);
    } else {
      invalidVars.push(varName);
    }
  }

  return { usedVars, invalidVars };
}

// Test expression with sample data
function testExpression(expression: string, context: any): TestResult {
  const code = expression.replace(/\{\{|\}\}/g, '').trim();

  try {
    // Create function with context
    const fn = new Function('$json', '$node', '$items', '$now', '$today',
      `return (${code})`);

    const result = fn(
      context.$json || {},
      context.$node || {},
      context.$items || (() => ({})),
      context.$now || new Date(),
      context.$today || new Date()
    );

    return { success: true, result };
  } catch (error) {
    return { success: false, error: error.message };
  }
}

常见错误与修复方法

访问未定义属性
// ERROR: Cannot read property 'email' of undefined
{{ $json.user.email }}

// FIX 1: Optional chaining
{{ $json.user?.email }}

// FIX 2: With fallback
{{ $json.user?.email ?? '[email protected]' }}

// FIX 3: Conditional
{{ $json.user ? $json.user.email : '' }}
类型错误
// ERROR: toLowerCase is not a function (when null)
{{ $json.name.toLowerCase() }}

// FIX: Null check first
{{ $json.name?.toLowerCase() ?? '' }}

// ERROR: toFixed is not a function (string instead of number)
{{ $json.price.toFixed(2) }}

// FIX: Parse as number first
{{ parseFloat($json.price).toFixed(2) }}

// ERROR: map is not a function (not an array)
{{ $json.items.map(i => i.name) }}

// FIX: Ensure array
{{ (Array.isArray($json.items) ? $json.items : []).map(i => i.name) }}
节点引用错误
// ERROR: Node "Previous Node" not found
{{ $node["Previous Node"].json.data }}

// FIX: Use exact node name (case-sensitive)
{{ $node["Previous Node1"].json.data }}

// FIX: Add fallback for safety
{{ $node["Previous Node"]?.json?.data ?? {} }}

安全模式

应避免的危险函数
// DANGEROUS: Never use eval
{{ eval($json.code) }}

// DANGEROUS: Dynamic function creation
{{ new Function($json.code)() }}

// DANGEROUS: setTimeout with string
{{ setTimeout($json.code, 1000) }}

// SAFE: Use explicit operations instead
{{ $json.value * 2 }}
{{ JSON.parse($json.jsonString) }}
输入验证
// Validate email format
{{ /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test($json.email) ? $json.email : '' }}

// Sanitize for HTML (basic)
{{ $json.text.replace(/[<>&"']/g, c => ({
  '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&#39;'
}[c])) }}

// Limit string length
{{ $json.input.substring(0, 1000) }}

// Validate number range
{{ Math.min(Math.max(parseInt($json.value), 0), 100) }}

性能优化

高效数组操作
// SLOW: Multiple iterations
{{ $json.items.filter(i => i.active).map(i => i.name).join(', ') }}

// FASTER: Single reduce
{{ $json.items.reduce((acc, i) => i.active ? (acc ? `${acc}, ${i.name}` : i.name) : acc, '') }}

// SLOW: Nested loops
{{ $json.items.map(i => $json.categories.find(c => c.id === i.categoryId)) }}

// FASTER: Create lookup map first (in Code node)
const categoryMap = Object.fromEntries($json.categories.map(c => [c.id, c]));
return $json.items.map(i => categoryMap[i.categoryId]);
在表达式中避免使用
// AVOID: Complex logic in expressions
{{ $json.items.reduce((acc, item) => {
  const category = $json.categories.find(c => c.id === item.catId);
  if (category && category.active) {
    acc.push({ ...item, categoryName: category.name });
  }
  return acc;
}, []) }}

// BETTER: Move to Code node for complex transformations

测试模式

// Expression test suite
const expressionTests = [
  {
    name: 'Basic property access',
    expression: '{{ $json.name }}',
    context: { $json: { name: 'John' } },
    expected: 'John'
  },
  {
    name: 'Nested with optional chaining',
    expression: '{{ $json.user?.email ?? "default" }}',
    context: { $json: { user: null } },
    expected: 'default'
  },
  {
    name: 'Array mapping',
    expression: '{{ $json.items.map(i => i.id).join(",") }}',
    context: { $json: { items: [{ id: 1 }, { id: 2 }] } },
    expected: '1,2'
  },
  {
    name: 'Conditional expression',
    expression: '{{ $json.score >= 70 ? "Pass" : "Fail" }}',
    context: { $json: { score: 85 } },
    expected: 'Pass'
  },
  {
    name: 'Node reference',
    expression: '{{ $node["Previous"].json.result }}',
    context: { $node: { Previous: { json: { result: 'success' } } } },
    expected: 'success'
  }
];

// Run tests
for (const test of expressionTests) {
  const result = testExpression(test.expression, test.context);
  console.log(`${test.name}: ${result.result === test.expected ? 'PASS' : 'FAIL'}`);
}

Agent 协调

内存命名空间
aqe/n8n/expressions/
├── validations/*    - Expression validation results
├── patterns/*       - Discovered expression patterns
├── errors/*         - Common error catalog
└── optimizations/*  - Performance suggestions
Fleet 协调
// Coordinate expression validation with workflow testing
await Task("Validate expressions", {
  workflowId: "wf-123",
  validateAll: true,
  testWithSampleData: true
}, "n8n-expression-validator");

相关技能


请记住

n8n 表达式类似 JavaScript,并包含特殊上下文变量($json、$node 等)。测试时需要:

  • 验证语法
  • 验证上下文变量
  • 检查空值安全性
  • 检查类型兼容性
  • 扫描安全问题

关键模式: 使用可选链(?.)和空值合并运算符(??)确保安全。将复杂逻辑移至 Code 节点。始终使用边缘情况(null、undefined、空数组)进行测试。

评论

支持 Markdown