@rog0x/mcp-testing-tools 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +250 -0
- package/dist/tools/api-mock.d.ts +30 -0
- package/dist/tools/api-mock.js +193 -0
- package/dist/tools/assertion-helper.d.ts +25 -0
- package/dist/tools/assertion-helper.js +223 -0
- package/dist/tools/mock-data.d.ts +14 -0
- package/dist/tools/mock-data.js +191 -0
- package/dist/tools/test-coverage-analyzer.d.ts +30 -0
- package/dist/tools/test-coverage-analyzer.js +247 -0
- package/dist/tools/test-generator.d.ts +7 -0
- package/dist/tools/test-generator.js +278 -0
- package/package.json +26 -0
- package/src/index.ts +311 -0
- package/src/tools/api-mock.ts +221 -0
- package/src/tools/assertion-helper.ts +273 -0
- package/src/tools/mock-data.ts +241 -0
- package/src/tools/test-coverage-analyzer.ts +283 -0
- package/src/tools/test-generator.ts +300 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse test files and source files to identify untested functions
|
|
3
|
+
* and suggest which functions need tests most, based on complexity
|
|
4
|
+
* and public API surface.
|
|
5
|
+
*/
|
|
6
|
+
function extractFunctions(sourceCode) {
|
|
7
|
+
const functions = [];
|
|
8
|
+
const lines = sourceCode.split("\n");
|
|
9
|
+
// Patterns for function declarations
|
|
10
|
+
const patterns = [
|
|
11
|
+
// export function name(...)
|
|
12
|
+
/^(\s*)(export\s+)?(async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
|
|
13
|
+
// export const name = (...) =>
|
|
14
|
+
/^(\s*)(export\s+)?(const|let|var)\s+(\w+)\s*=\s*(async\s+)?\(([^)]*)\)\s*(:\s*\S+)?\s*=>/,
|
|
15
|
+
// export const name = function(...)
|
|
16
|
+
/^(\s*)(export\s+)?(const|let|var)\s+(\w+)\s*=\s*(async\s+)?function\s*\(([^)]*)\)/,
|
|
17
|
+
// class method: name(...) or async name(...)
|
|
18
|
+
/^(\s+)(public\s+|private\s+|protected\s+)?(static\s+)?(async\s+)?(\w+)\s*\(([^)]*)\)\s*(:\s*\S+)?\s*\{/,
|
|
19
|
+
];
|
|
20
|
+
for (let i = 0; i < lines.length; i++) {
|
|
21
|
+
const line = lines[i];
|
|
22
|
+
// Standard function declaration
|
|
23
|
+
const funcMatch = line.match(patterns[0]);
|
|
24
|
+
if (funcMatch) {
|
|
25
|
+
const name = funcMatch[4];
|
|
26
|
+
const isExported = !!funcMatch[2];
|
|
27
|
+
const isAsync = !!funcMatch[3];
|
|
28
|
+
const params = funcMatch[5].trim();
|
|
29
|
+
const paramCount = params ? params.split(",").length : 0;
|
|
30
|
+
const complexity = estimateComplexity(sourceCode, i, lines);
|
|
31
|
+
functions.push({
|
|
32
|
+
name,
|
|
33
|
+
line: i + 1,
|
|
34
|
+
isExported,
|
|
35
|
+
isAsync,
|
|
36
|
+
paramCount,
|
|
37
|
+
estimatedComplexity: complexity,
|
|
38
|
+
hasBranching: complexity > 1,
|
|
39
|
+
});
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// Arrow function
|
|
43
|
+
const arrowMatch = line.match(patterns[1]);
|
|
44
|
+
if (arrowMatch) {
|
|
45
|
+
const name = arrowMatch[4];
|
|
46
|
+
const isExported = !!arrowMatch[2];
|
|
47
|
+
const isAsync = !!arrowMatch[5];
|
|
48
|
+
const params = arrowMatch[6].trim();
|
|
49
|
+
const paramCount = params ? params.split(",").length : 0;
|
|
50
|
+
const complexity = estimateComplexity(sourceCode, i, lines);
|
|
51
|
+
functions.push({
|
|
52
|
+
name,
|
|
53
|
+
line: i + 1,
|
|
54
|
+
isExported,
|
|
55
|
+
isAsync,
|
|
56
|
+
paramCount,
|
|
57
|
+
estimatedComplexity: complexity,
|
|
58
|
+
hasBranching: complexity > 1,
|
|
59
|
+
});
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
// Function expression
|
|
63
|
+
const exprMatch = line.match(patterns[2]);
|
|
64
|
+
if (exprMatch) {
|
|
65
|
+
const name = exprMatch[4];
|
|
66
|
+
const isExported = !!exprMatch[2];
|
|
67
|
+
const isAsync = !!exprMatch[5];
|
|
68
|
+
const params = exprMatch[6].trim();
|
|
69
|
+
const paramCount = params ? params.split(",").length : 0;
|
|
70
|
+
const complexity = estimateComplexity(sourceCode, i, lines);
|
|
71
|
+
functions.push({
|
|
72
|
+
name,
|
|
73
|
+
line: i + 1,
|
|
74
|
+
isExported,
|
|
75
|
+
isAsync,
|
|
76
|
+
paramCount,
|
|
77
|
+
estimatedComplexity: complexity,
|
|
78
|
+
hasBranching: complexity > 1,
|
|
79
|
+
});
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
// Class method
|
|
83
|
+
const methodMatch = line.match(patterns[3]);
|
|
84
|
+
if (methodMatch) {
|
|
85
|
+
const name = methodMatch[5];
|
|
86
|
+
if (name === "constructor" || name === "if" || name === "for" || name === "while" || name === "switch")
|
|
87
|
+
continue;
|
|
88
|
+
const visibility = (methodMatch[2] || "").trim();
|
|
89
|
+
const isExported = visibility !== "private";
|
|
90
|
+
const isAsync = !!methodMatch[4];
|
|
91
|
+
const params = methodMatch[6].trim();
|
|
92
|
+
const paramCount = params ? params.split(",").length : 0;
|
|
93
|
+
const complexity = estimateComplexity(sourceCode, i, lines);
|
|
94
|
+
functions.push({
|
|
95
|
+
name,
|
|
96
|
+
line: i + 1,
|
|
97
|
+
isExported,
|
|
98
|
+
isAsync,
|
|
99
|
+
paramCount,
|
|
100
|
+
estimatedComplexity: complexity,
|
|
101
|
+
hasBranching: complexity > 1,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return functions;
|
|
106
|
+
}
|
|
107
|
+
function estimateComplexity(source, startLine, lines) {
|
|
108
|
+
// Count branching keywords in the function body (approximate)
|
|
109
|
+
let complexity = 1; // base complexity
|
|
110
|
+
let braceDepth = 0;
|
|
111
|
+
let started = false;
|
|
112
|
+
for (let i = startLine; i < Math.min(startLine + 200, lines.length); i++) {
|
|
113
|
+
const line = lines[i];
|
|
114
|
+
for (const ch of line) {
|
|
115
|
+
if (ch === "{") {
|
|
116
|
+
braceDepth++;
|
|
117
|
+
started = true;
|
|
118
|
+
}
|
|
119
|
+
if (ch === "}")
|
|
120
|
+
braceDepth--;
|
|
121
|
+
}
|
|
122
|
+
if (started && braceDepth === 0)
|
|
123
|
+
break;
|
|
124
|
+
// Count branching constructs
|
|
125
|
+
if (/\bif\s*\(/.test(line))
|
|
126
|
+
complexity++;
|
|
127
|
+
if (/\belse\s+if\s*\(/.test(line))
|
|
128
|
+
complexity++;
|
|
129
|
+
if (/\belse\s*\{/.test(line))
|
|
130
|
+
complexity++;
|
|
131
|
+
if (/\bfor\s*\(/.test(line))
|
|
132
|
+
complexity++;
|
|
133
|
+
if (/\bwhile\s*\(/.test(line))
|
|
134
|
+
complexity++;
|
|
135
|
+
if (/\bswitch\s*\(/.test(line))
|
|
136
|
+
complexity++;
|
|
137
|
+
if (/\bcase\s+/.test(line))
|
|
138
|
+
complexity++;
|
|
139
|
+
if (/\bcatch\s*\(/.test(line))
|
|
140
|
+
complexity++;
|
|
141
|
+
if (/\?\?/.test(line))
|
|
142
|
+
complexity++;
|
|
143
|
+
if (/\?\./.test(line))
|
|
144
|
+
complexity++;
|
|
145
|
+
if (/\b\?\s/.test(line) && /:/.test(line))
|
|
146
|
+
complexity++; // ternary
|
|
147
|
+
}
|
|
148
|
+
return complexity;
|
|
149
|
+
}
|
|
150
|
+
function extractTestedNames(testCode) {
|
|
151
|
+
const tested = new Set();
|
|
152
|
+
// Match patterns like: describe("functionName" / it("should ... functionName"
|
|
153
|
+
const describeMatches = testCode.matchAll(/describe\s*\(\s*["'`]([^"'`]+)["'`]/g);
|
|
154
|
+
for (const m of describeMatches) {
|
|
155
|
+
const name = m[1].trim();
|
|
156
|
+
// If it looks like a function name (single word, camelCase, etc.)
|
|
157
|
+
if (/^\w+$/.test(name)) {
|
|
158
|
+
tested.add(name);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Match direct function calls in test blocks
|
|
162
|
+
const callMatches = testCode.matchAll(/(?:expect|assert)\s*\(\s*(?:await\s+)?(\w+)\s*\(/g);
|
|
163
|
+
for (const m of callMatches) {
|
|
164
|
+
tested.add(m[1]);
|
|
165
|
+
}
|
|
166
|
+
// Match imports
|
|
167
|
+
const importMatches = testCode.matchAll(/import\s*\{([^}]+)\}/g);
|
|
168
|
+
for (const m of importMatches) {
|
|
169
|
+
const names = m[1].split(",").map((n) => n.trim().split(/\s+as\s+/)[0].trim());
|
|
170
|
+
for (const name of names) {
|
|
171
|
+
if (name)
|
|
172
|
+
tested.add(name);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Match require destructuring
|
|
176
|
+
const requireMatches = testCode.matchAll(/const\s*\{([^}]+)\}\s*=\s*require/g);
|
|
177
|
+
for (const m of requireMatches) {
|
|
178
|
+
const names = m[1].split(",").map((n) => n.trim().split(/\s*:\s*/)[0].trim());
|
|
179
|
+
for (const name of names) {
|
|
180
|
+
if (name)
|
|
181
|
+
tested.add(name);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return Array.from(tested);
|
|
185
|
+
}
|
|
186
|
+
function prioritize(fn) {
|
|
187
|
+
const reasons = [];
|
|
188
|
+
let score = 0;
|
|
189
|
+
if (fn.isExported) {
|
|
190
|
+
score += 3;
|
|
191
|
+
reasons.push("exported/public API");
|
|
192
|
+
}
|
|
193
|
+
if (fn.estimatedComplexity > 5) {
|
|
194
|
+
score += 3;
|
|
195
|
+
reasons.push(`high complexity (${fn.estimatedComplexity})`);
|
|
196
|
+
}
|
|
197
|
+
else if (fn.estimatedComplexity > 2) {
|
|
198
|
+
score += 2;
|
|
199
|
+
reasons.push(`moderate complexity (${fn.estimatedComplexity})`);
|
|
200
|
+
}
|
|
201
|
+
if (fn.paramCount >= 3) {
|
|
202
|
+
score += 1;
|
|
203
|
+
reasons.push(`${fn.paramCount} parameters`);
|
|
204
|
+
}
|
|
205
|
+
if (fn.hasBranching) {
|
|
206
|
+
score += 1;
|
|
207
|
+
reasons.push("contains branching logic");
|
|
208
|
+
}
|
|
209
|
+
if (fn.isAsync) {
|
|
210
|
+
score += 1;
|
|
211
|
+
reasons.push("async (potential error paths)");
|
|
212
|
+
}
|
|
213
|
+
const priority = score >= 5 ? "high" : score >= 3 ? "medium" : "low";
|
|
214
|
+
return { priority, reason: reasons.join("; ") };
|
|
215
|
+
}
|
|
216
|
+
export function analyzeCoverage(sourceCode, testCode, sourceFileName = "source.ts") {
|
|
217
|
+
const functions = extractFunctions(sourceCode);
|
|
218
|
+
const testedNames = extractTestedNames(testCode);
|
|
219
|
+
const testedFunctions = [];
|
|
220
|
+
const untestedFunctions = [];
|
|
221
|
+
for (const fn of functions) {
|
|
222
|
+
if (testedNames.includes(fn.name)) {
|
|
223
|
+
testedFunctions.push(fn.name);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
untestedFunctions.push(fn);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const suggestions = untestedFunctions
|
|
230
|
+
.map((fn) => {
|
|
231
|
+
const { priority, reason } = prioritize(fn);
|
|
232
|
+
return { functionName: fn.name, priority, reason };
|
|
233
|
+
})
|
|
234
|
+
.sort((a, b) => {
|
|
235
|
+
const order = { high: 0, medium: 1, low: 2 };
|
|
236
|
+
return order[a.priority] - order[b.priority];
|
|
237
|
+
});
|
|
238
|
+
return {
|
|
239
|
+
sourceFile: sourceFileName,
|
|
240
|
+
totalFunctions: functions.length,
|
|
241
|
+
exportedFunctions: functions.filter((f) => f.isExported).length,
|
|
242
|
+
testedFunctions,
|
|
243
|
+
untestedFunctions,
|
|
244
|
+
suggestions,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=test-coverage-analyzer.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate test cases from a function signature.
|
|
3
|
+
* Produces happy path, edge cases, error cases, and boundary value tests
|
|
4
|
+
* as Jest/Vitest-compatible test code.
|
|
5
|
+
*/
|
|
6
|
+
export declare function generateTests(signature: string, framework?: string, modulePath?: string): string;
|
|
7
|
+
//# sourceMappingURL=test-generator.d.ts.map
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate test cases from a function signature.
|
|
3
|
+
* Produces happy path, edge cases, error cases, and boundary value tests
|
|
4
|
+
* as Jest/Vitest-compatible test code.
|
|
5
|
+
*/
|
|
6
|
+
function parseSignature(signature) {
|
|
7
|
+
const asyncMatch = signature.trim().startsWith("async");
|
|
8
|
+
const cleaned = signature.replace(/^(export\s+)?(async\s+)?function\s+/, "").trim();
|
|
9
|
+
const nameMatch = cleaned.match(/^(\w+)/);
|
|
10
|
+
const name = nameMatch ? nameMatch[1] : "unknownFunction";
|
|
11
|
+
const paramsMatch = cleaned.match(/\(([^)]*)\)/);
|
|
12
|
+
const paramsStr = paramsMatch ? paramsMatch[1].trim() : "";
|
|
13
|
+
const returnMatch = cleaned.match(/\):\s*(.+?)(\s*\{|$)/);
|
|
14
|
+
const returnType = returnMatch ? returnMatch[1].trim() : "void";
|
|
15
|
+
const params = [];
|
|
16
|
+
if (paramsStr) {
|
|
17
|
+
const paramParts = splitParams(paramsStr);
|
|
18
|
+
for (const part of paramParts) {
|
|
19
|
+
const trimmed = part.trim();
|
|
20
|
+
if (!trimmed)
|
|
21
|
+
continue;
|
|
22
|
+
const optional = trimmed.includes("?:");
|
|
23
|
+
const defaultMatch = trimmed.match(/=\s*(.+)$/);
|
|
24
|
+
const cleanPart = trimmed.replace(/\s*=\s*.+$/, "").trim();
|
|
25
|
+
const colonIdx = cleanPart.indexOf(":");
|
|
26
|
+
let paramName;
|
|
27
|
+
let paramType;
|
|
28
|
+
if (colonIdx !== -1) {
|
|
29
|
+
paramName = cleanPart.slice(0, colonIdx).replace("?", "").trim();
|
|
30
|
+
paramType = cleanPart.slice(colonIdx + 1).trim();
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
paramName = cleanPart.replace("?", "").trim();
|
|
34
|
+
paramType = "unknown";
|
|
35
|
+
}
|
|
36
|
+
params.push({
|
|
37
|
+
name: paramName,
|
|
38
|
+
type: paramType,
|
|
39
|
+
optional: optional || !!defaultMatch,
|
|
40
|
+
defaultValue: defaultMatch ? defaultMatch[1].trim() : undefined,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { name, params, returnType, isAsync: asyncMatch };
|
|
45
|
+
}
|
|
46
|
+
function splitParams(str) {
|
|
47
|
+
const results = [];
|
|
48
|
+
let depth = 0;
|
|
49
|
+
let current = "";
|
|
50
|
+
for (const ch of str) {
|
|
51
|
+
if (ch === "<" || ch === "(" || ch === "[" || ch === "{")
|
|
52
|
+
depth++;
|
|
53
|
+
if (ch === ">" || ch === ")" || ch === "]" || ch === "}")
|
|
54
|
+
depth--;
|
|
55
|
+
if (ch === "," && depth === 0) {
|
|
56
|
+
results.push(current);
|
|
57
|
+
current = "";
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
current += ch;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (current.trim())
|
|
64
|
+
results.push(current);
|
|
65
|
+
return results;
|
|
66
|
+
}
|
|
67
|
+
function sampleValueForType(type, variant) {
|
|
68
|
+
const t = type.toLowerCase().replace(/\s/g, "");
|
|
69
|
+
if (t === "string" || t.includes("string")) {
|
|
70
|
+
switch (variant) {
|
|
71
|
+
case "happy": return '"hello world"';
|
|
72
|
+
case "edge": return '""';
|
|
73
|
+
case "boundary": return '"a".repeat(10000)';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (t === "number" || t.includes("number")) {
|
|
77
|
+
switch (variant) {
|
|
78
|
+
case "happy": return "42";
|
|
79
|
+
case "edge": return "0";
|
|
80
|
+
case "boundary": return "Number.MAX_SAFE_INTEGER";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (t === "boolean" || t.includes("boolean")) {
|
|
84
|
+
return variant === "happy" ? "true" : "false";
|
|
85
|
+
}
|
|
86
|
+
if (t.endsWith("[]") || t.startsWith("array")) {
|
|
87
|
+
switch (variant) {
|
|
88
|
+
case "happy": return "[1, 2, 3]";
|
|
89
|
+
case "edge": return "[]";
|
|
90
|
+
case "boundary": return "Array.from({ length: 10000 }, (_, i) => i)";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (t === "object" || t.startsWith("{")) {
|
|
94
|
+
switch (variant) {
|
|
95
|
+
case "happy": return '{ key: "value" }';
|
|
96
|
+
case "edge": return "{}";
|
|
97
|
+
case "boundary": return "Object.create(null)";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (t === "date") {
|
|
101
|
+
switch (variant) {
|
|
102
|
+
case "happy": return 'new Date("2025-06-15")';
|
|
103
|
+
case "edge": return "new Date(0)";
|
|
104
|
+
case "boundary": return 'new Date("9999-12-31")';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (t.includes("|")) {
|
|
108
|
+
const parts = t.split("|").map((p) => p.trim());
|
|
109
|
+
return sampleValueForType(parts[0], variant);
|
|
110
|
+
}
|
|
111
|
+
if (t === "null")
|
|
112
|
+
return "null";
|
|
113
|
+
if (t === "undefined")
|
|
114
|
+
return "undefined";
|
|
115
|
+
return variant === "happy" ? "{}" : "undefined";
|
|
116
|
+
}
|
|
117
|
+
function buildArgs(params, variant, omitOptional) {
|
|
118
|
+
const args = [];
|
|
119
|
+
for (const p of params) {
|
|
120
|
+
if (omitOptional && p.optional)
|
|
121
|
+
continue;
|
|
122
|
+
args.push(sampleValueForType(p.type, variant));
|
|
123
|
+
}
|
|
124
|
+
return args.join(", ");
|
|
125
|
+
}
|
|
126
|
+
function generateReturnAssertion(returnType, callExpr, isAsync) {
|
|
127
|
+
const awaited = isAsync ? `await ${callExpr}` : callExpr;
|
|
128
|
+
const t = returnType.toLowerCase().replace(/\s/g, "");
|
|
129
|
+
if (t === "void" || t === "promise<void>") {
|
|
130
|
+
return ` expect(${awaited}).toBeUndefined();`;
|
|
131
|
+
}
|
|
132
|
+
if (t === "boolean" || t === "promise<boolean>") {
|
|
133
|
+
return ` expect(typeof (${awaited})).toBe("boolean");`;
|
|
134
|
+
}
|
|
135
|
+
if (t === "string" || t === "promise<string>") {
|
|
136
|
+
return ` expect(typeof (${awaited})).toBe("string");`;
|
|
137
|
+
}
|
|
138
|
+
if (t === "number" || t === "promise<number>") {
|
|
139
|
+
return ` expect(typeof (${awaited})).toBe("number");`;
|
|
140
|
+
}
|
|
141
|
+
if (t.endsWith("[]") || t.startsWith("array") || t.includes("promise<") && t.includes("[]")) {
|
|
142
|
+
return ` const result = ${awaited};\n expect(Array.isArray(result)).toBe(true);`;
|
|
143
|
+
}
|
|
144
|
+
return ` const result = ${awaited};\n expect(result).toBeDefined();`;
|
|
145
|
+
}
|
|
146
|
+
export function generateTests(signature, framework = "vitest", modulePath = "./module") {
|
|
147
|
+
const parsed = parseSignature(signature);
|
|
148
|
+
const fn = parsed.isAsync ? "async " : "";
|
|
149
|
+
const importStatement = framework === "jest"
|
|
150
|
+
? `const { ${parsed.name} } = require("${modulePath}");`
|
|
151
|
+
: `import { ${parsed.name} } from "${modulePath}";`;
|
|
152
|
+
const lines = [];
|
|
153
|
+
lines.push(importStatement);
|
|
154
|
+
lines.push("");
|
|
155
|
+
lines.push(`describe("${parsed.name}", () => {`);
|
|
156
|
+
// Happy path
|
|
157
|
+
lines.push(` describe("happy path", () => {`);
|
|
158
|
+
const happyArgs = buildArgs(parsed.params, "happy", false);
|
|
159
|
+
lines.push(` it("should return expected result with valid inputs", ${fn}() => {`);
|
|
160
|
+
lines.push(generateReturnAssertion(parsed.returnType, `${parsed.name}(${happyArgs})`, parsed.isAsync));
|
|
161
|
+
lines.push(" });");
|
|
162
|
+
if (parsed.params.some((p) => p.optional)) {
|
|
163
|
+
const minArgs = buildArgs(parsed.params, "happy", true);
|
|
164
|
+
lines.push("");
|
|
165
|
+
lines.push(` it("should work with only required parameters", ${fn}() => {`);
|
|
166
|
+
lines.push(generateReturnAssertion(parsed.returnType, `${parsed.name}(${minArgs})`, parsed.isAsync));
|
|
167
|
+
lines.push(" });");
|
|
168
|
+
}
|
|
169
|
+
lines.push(" });");
|
|
170
|
+
// Edge cases
|
|
171
|
+
lines.push("");
|
|
172
|
+
lines.push(` describe("edge cases", () => {`);
|
|
173
|
+
const edgeArgs = buildArgs(parsed.params, "edge", false);
|
|
174
|
+
lines.push(` it("should handle edge-case inputs gracefully", ${fn}() => {`);
|
|
175
|
+
if (parsed.isAsync) {
|
|
176
|
+
lines.push(` await expect(${parsed.name}(${edgeArgs})).resolves.toBeDefined();`);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
lines.push(` expect(() => ${parsed.name}(${edgeArgs})).not.toThrow();`);
|
|
180
|
+
}
|
|
181
|
+
lines.push(" });");
|
|
182
|
+
// null/undefined for each param
|
|
183
|
+
for (const param of parsed.params) {
|
|
184
|
+
if (!param.optional) {
|
|
185
|
+
lines.push("");
|
|
186
|
+
lines.push(` it("should handle null ${param.name}", ${fn}() => {`);
|
|
187
|
+
const nullArgs = parsed.params.map((p) => (p.name === param.name ? "null as any" : sampleValueForType(p.type, "happy"))).join(", ");
|
|
188
|
+
if (parsed.isAsync) {
|
|
189
|
+
lines.push(` await expect(${parsed.name}(${nullArgs})).rejects.toThrow();`);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
lines.push(` expect(() => ${parsed.name}(${nullArgs})).toThrow();`);
|
|
193
|
+
}
|
|
194
|
+
lines.push(" });");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
lines.push(" });");
|
|
198
|
+
// Error cases
|
|
199
|
+
lines.push("");
|
|
200
|
+
lines.push(` describe("error cases", () => {`);
|
|
201
|
+
if (parsed.params.length > 0) {
|
|
202
|
+
lines.push(` it("should throw when called with no arguments", ${fn}() => {`);
|
|
203
|
+
if (parsed.isAsync) {
|
|
204
|
+
lines.push(` // @ts-expect-error testing missing arguments`);
|
|
205
|
+
lines.push(` await expect(${parsed.name}()).rejects.toThrow();`);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
lines.push(` // @ts-expect-error testing missing arguments`);
|
|
209
|
+
lines.push(` expect(() => ${parsed.name}()).toThrow();`);
|
|
210
|
+
}
|
|
211
|
+
lines.push(" });");
|
|
212
|
+
lines.push("");
|
|
213
|
+
lines.push(` it("should throw with wrong argument types", ${fn}() => {`);
|
|
214
|
+
const wrongArgs = parsed.params.map(() => "Symbol() as any").join(", ");
|
|
215
|
+
if (parsed.isAsync) {
|
|
216
|
+
lines.push(` await expect(${parsed.name}(${wrongArgs})).rejects.toThrow();`);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
lines.push(` expect(() => ${parsed.name}(${wrongArgs})).toThrow();`);
|
|
220
|
+
}
|
|
221
|
+
lines.push(" });");
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
lines.push(` // No parameters — limited error scenarios`);
|
|
225
|
+
lines.push(` it("should not throw when called", ${fn}() => {`);
|
|
226
|
+
if (parsed.isAsync) {
|
|
227
|
+
lines.push(` await expect(${parsed.name}()).resolves.toBeDefined();`);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
lines.push(` expect(() => ${parsed.name}()).not.toThrow();`);
|
|
231
|
+
}
|
|
232
|
+
lines.push(" });");
|
|
233
|
+
}
|
|
234
|
+
lines.push(" });");
|
|
235
|
+
// Boundary values
|
|
236
|
+
lines.push("");
|
|
237
|
+
lines.push(` describe("boundary values", () => {`);
|
|
238
|
+
const boundaryArgs = buildArgs(parsed.params, "boundary", false);
|
|
239
|
+
lines.push(` it("should handle extreme values", ${fn}() => {`);
|
|
240
|
+
if (parsed.isAsync) {
|
|
241
|
+
lines.push(` await expect(${parsed.name}(${boundaryArgs})).resolves.toBeDefined();`);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
lines.push(` expect(() => ${parsed.name}(${boundaryArgs})).not.toThrow();`);
|
|
245
|
+
}
|
|
246
|
+
lines.push(" });");
|
|
247
|
+
// Negative numbers for number params
|
|
248
|
+
const numericParams = parsed.params.filter((p) => p.type.toLowerCase().includes("number"));
|
|
249
|
+
for (const param of numericParams) {
|
|
250
|
+
lines.push("");
|
|
251
|
+
lines.push(` it("should handle negative value for ${param.name}", ${fn}() => {`);
|
|
252
|
+
const negArgs = parsed.params.map((p) => (p.name === param.name ? "-1" : sampleValueForType(p.type, "happy"))).join(", ");
|
|
253
|
+
lines.push(` const call = () => ${parsed.name}(${negArgs});`);
|
|
254
|
+
if (parsed.isAsync) {
|
|
255
|
+
lines.push(` // Depending on implementation, may resolve or reject`);
|
|
256
|
+
lines.push(` await expect(call()).resolves.toBeDefined();`);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
lines.push(` // Depending on implementation, may return or throw`);
|
|
260
|
+
lines.push(` expect(call).not.toThrow();`);
|
|
261
|
+
}
|
|
262
|
+
lines.push(" });");
|
|
263
|
+
lines.push("");
|
|
264
|
+
lines.push(` it("should handle NaN for ${param.name}", ${fn}() => {`);
|
|
265
|
+
const nanArgs = parsed.params.map((p) => (p.name === param.name ? "NaN" : sampleValueForType(p.type, "happy"))).join(", ");
|
|
266
|
+
if (parsed.isAsync) {
|
|
267
|
+
lines.push(` await expect(${parsed.name}(${nanArgs})).rejects.toThrow();`);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
lines.push(` expect(() => ${parsed.name}(${nanArgs})).toThrow();`);
|
|
271
|
+
}
|
|
272
|
+
lines.push(" });");
|
|
273
|
+
}
|
|
274
|
+
lines.push(" });");
|
|
275
|
+
lines.push("});");
|
|
276
|
+
return lines.join("\n");
|
|
277
|
+
}
|
|
278
|
+
//# sourceMappingURL=test-generator.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rog0x/mcp-testing-tools",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Testing and quality assurance tools for AI agents via MCP",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"start": "node dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"mcp",
|
|
13
|
+
"testing",
|
|
14
|
+
"quality-assurance",
|
|
15
|
+
"mock-data",
|
|
16
|
+
"test-generation"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"typescript": "^5.7.3",
|
|
24
|
+
"@types/node": "^22.15.2"
|
|
25
|
+
}
|
|
26
|
+
}
|