qe-n8n-expression-testing について
n8n の式をテストするスキルです。構文、コンテキスト変数、null 処理、型安全性、セキュリティを確認し、よくあるエラーへの対処例も示します。
主な機能
- 01式の構文を検証
- 02コンテキスト変数の参照を確認
- 03null と型の処理をテスト
- 04危険な関数を検出
- 05配列処理の性能改善を提案
活用シーン
- 01ワークフロー内の式を検証
- 02ネストしたプロパティ参照のエラーを調査
- 03式のセキュリティリスクを確認
- 04大規模データの配列処理を最適化
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ proffesor-for-testing/ agentic-qe --skill qe-n8n-expression-testingインストール後、Claude Code で/qe-n8n-expression-testingを入力して読み込まれたか確認します。
詳細
n8n Expression Testing
<default_to_action> n8n の式をテストする際は、次の手順に従います。
- 実行前に構文を検証する
- 複数のコンテキストシナリオでテストする
- null/undefined の処理を確認する
- 型安全性を検証する
- セキュリティの脆弱性を調査する
式のクイックチェックリスト:
- 有効な JavaScript 構文
- コンテキスト変数を正しく参照($json、$node)
- null 安全なアクセスパターン(?.、??)
- 危険な関数(eval、Function)を使用していない
- 大規模なデータセットでも効率的
よくある落とし穴:
- null チェックなしでネストされたプロパティにアクセスする
- 型強制による問題
- フォールバック値の設定漏れ
- 効率の悪い配列操作 </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 => ({
'<': '<', '>': '>', '&': '&', '"': '"', "'": '''
}[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'}`);
}
エージェントの連携
メモリ名前空間
aqe/n8n/expressions/
├── validations/* - Expression validation results
├── patterns/* - Discovered expression patterns
├── errors/* - Common error catalog
└── optimizations/* - Performance suggestions
フリートの連携
// Coordinate expression validation with workflow testing
await Task("Validate expressions", {
workflowId: "wf-123",
validateAll: true,
testWithSampleData: true
}, "n8n-expression-validator");
関連スキル
- n8n-workflow-testing-fundamentals - ワークフローのテスト
- n8n-security-testing - セキュリティ検証
覚えておくこと
n8n の式は JavaScript に似た構文で、特別なコンテキスト変数($json、$node など)を使用します。テストでは次の項目が必要です。
- 構文の検証
- コンテキスト変数の確認
- null 安全性のチェック
- 型の互換性
- セキュリティの調査
重要なパターン: 安全性を確保するには、オプショナルチェーン(?.)と null 合体演算子(??)を使用します。複雑なロジックは Code ノードに移します。エッジケース(null、undefined、空の配列)を使って必ずテストします。
コメント
Markdown 対応