Skip to content

Commit 543bbab

Browse files
committed
feat: 流程测试用例 - 保存/执行/断言校验/批量执行,支持结果查看(需求13)
1 parent 6d8b716 commit 543bbab

3 files changed

Lines changed: 527 additions & 0 deletions

File tree

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
using System.Text.Json;
2+
using Juggle.Application.Models.Request;
3+
using Juggle.Application.Models.Response;
4+
using Juggle.Application.Services.Flow;
5+
using Juggle.Domain.Entities;
6+
using Juggle.Infrastructure.Persistence;
7+
using Microsoft.AspNetCore.Authorization;
8+
using Microsoft.AspNetCore.Mvc;
9+
using Microsoft.EntityFrameworkCore;
10+
11+
namespace Juggle.Api.Controllers.Api;
12+
13+
[ApiController]
14+
[Route("api/flow/testcase")]
15+
[Authorize]
16+
public class FlowTestCaseController : ControllerBase
17+
{
18+
private readonly JuggleDbContext _db;
19+
private readonly FlowExecutionService _flowExec;
20+
21+
public FlowTestCaseController(JuggleDbContext db, FlowExecutionService flowExec)
22+
{
23+
_db = db;
24+
_flowExec = flowExec;
25+
}
26+
27+
[HttpPost("page")]
28+
public async Task<ApiResult> Page([FromBody] FlowTestCasePageRequest req)
29+
{
30+
var query = _db.FlowTestCases.Where(c => c.Deleted == 0);
31+
if (!string.IsNullOrEmpty(req.FlowKey))
32+
query = query.Where(c => c.FlowKey == req.FlowKey);
33+
if (!string.IsNullOrEmpty(req.Keyword))
34+
query = query.Where(c => c.CaseName.Contains(req.Keyword));
35+
var total = await query.CountAsync();
36+
var records = await query.OrderByDescending(c => c.Id)
37+
.Skip((req.PageNum - 1) * req.PageSize).Take(req.PageSize)
38+
.ToListAsync();
39+
return ApiResult.Success(new { total, records });
40+
}
41+
42+
[HttpPost("save")]
43+
public async Task<ApiResult> Save([FromBody] FlowTestCaseSaveRequest req)
44+
{
45+
if (req.Id.HasValue && req.Id > 0)
46+
{
47+
var entity = await _db.FlowTestCases.FindAsync(req.Id.Value);
48+
if (entity == null) return ApiResult.Fail("用例不存在");
49+
entity.CaseName = req.CaseName;
50+
entity.InputJson = req.InputJson;
51+
entity.AssertJson= req.AssertJson;
52+
entity.Remark = req.Remark;
53+
entity.UpdatedAt = DateTime.Now.ToString("o");
54+
}
55+
else
56+
{
57+
_db.FlowTestCases.Add(new FlowTestCaseEntity
58+
{
59+
FlowKey = req.FlowKey,
60+
CaseName = req.CaseName,
61+
InputJson = req.InputJson,
62+
AssertJson = req.AssertJson,
63+
LastRunStatus = "PENDING",
64+
Remark = req.Remark,
65+
Deleted = 0,
66+
CreatedAt = DateTime.Now.ToString("o")
67+
});
68+
}
69+
await _db.SaveChangesAsync();
70+
return ApiResult.Success();
71+
}
72+
73+
[HttpDelete("delete/{id}")]
74+
public async Task<ApiResult> Delete(long id)
75+
{
76+
var entity = await _db.FlowTestCases.FindAsync(id);
77+
if (entity == null) return ApiResult.Fail("用例不存在");
78+
entity.Deleted = 1;
79+
entity.UpdatedAt = DateTime.Now.ToString("o");
80+
await _db.SaveChangesAsync();
81+
return ApiResult.Success();
82+
}
83+
84+
/// <summary>执行测试用例并进行断言校验</summary>
85+
[HttpPost("run/{id}")]
86+
public async Task<ApiResult> Run(long id)
87+
{
88+
var testCase = await _db.FlowTestCases.FindAsync(id);
89+
if (testCase == null || testCase.Deleted == 1) return ApiResult.Fail("用例不存在");
90+
91+
var definition = await _db.FlowDefinitions
92+
.FirstOrDefaultAsync(d => d.FlowKey == testCase.FlowKey && d.Deleted == 0);
93+
if (definition == null) return ApiResult.Fail("流程不存在");
94+
if (string.IsNullOrEmpty(definition.FlowContent) || definition.FlowContent == "[]")
95+
return ApiResult.Fail("流程内容为空");
96+
97+
// 解析入参
98+
Dictionary<string, object?> inputParams = new();
99+
if (!string.IsNullOrEmpty(testCase.InputJson))
100+
{
101+
try
102+
{
103+
var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
104+
inputParams = JsonSerializer.Deserialize<Dictionary<string, object?>>(testCase.InputJson, opts) ?? new();
105+
}
106+
catch { }
107+
}
108+
109+
// 执行流程
110+
var execResult = await _flowExec.RunAsync(definition, definition.FlowContent!, inputParams, "testcase");
111+
112+
// 断言校验
113+
var assertResults = new List<object>();
114+
bool allPassed = true;
115+
116+
if (!string.IsNullOrEmpty(testCase.AssertJson) && execResult.Success)
117+
{
118+
try
119+
{
120+
var asserts = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(testCase.AssertJson,
121+
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new();
122+
123+
foreach (var (varName, expectedVal) in asserts)
124+
{
125+
string? expected = expectedVal.ValueKind == JsonValueKind.String
126+
? expectedVal.GetString()
127+
: expectedVal.GetRawText();
128+
129+
string? actual = null;
130+
if (execResult.OutputData?.TryGetValue(varName, out var actualObj) == true)
131+
actual = actualObj?.ToString();
132+
133+
bool passed = actual == expected;
134+
if (!passed) allPassed = false;
135+
assertResults.Add(new { varName, expected, actual, passed });
136+
}
137+
}
138+
catch (Exception ex)
139+
{
140+
assertResults.Add(new { varName = "_parse_error", expected = "", actual = ex.Message, passed = false });
141+
allPassed = false;
142+
}
143+
}
144+
145+
var finalStatus = execResult.Success && allPassed ? "SUCCESS" : "FAILED";
146+
var summary = execResult.Success
147+
? (assertResults.Count > 0 ? $"断言 {assertResults.Count(a => ((dynamic)a).passed)} / {assertResults.Count} 通过" : "执行成功(无断言)")
148+
: $"执行失败: {execResult.ErrorMessage}";
149+
150+
// 更新用例状态
151+
testCase.LastRunStatus = finalStatus;
152+
testCase.LastRunTime = DateTime.Now.ToString("o");
153+
testCase.LastRunResult = summary;
154+
testCase.UpdatedAt = DateTime.Now.ToString("o");
155+
await _db.SaveChangesAsync();
156+
157+
return ApiResult.Success(new
158+
{
159+
success = execResult.Success && allPassed,
160+
status = finalStatus,
161+
summary,
162+
errorMessage = execResult.ErrorMessage,
163+
outputs = execResult.OutputData,
164+
assertResults,
165+
logId = execResult.LogId,
166+
costMs = execResult.CostMs
167+
});
168+
}
169+
170+
/// <summary>批量执行某个流程的所有测试用例</summary>
171+
[HttpPost("runAll/{flowKey}")]
172+
public async Task<ApiResult> RunAll(string flowKey)
173+
{
174+
var cases = await _db.FlowTestCases
175+
.Where(c => c.FlowKey == flowKey && c.Deleted == 0)
176+
.ToListAsync();
177+
178+
if (cases.Count == 0) return ApiResult.Fail("该流程暂无测试用例");
179+
180+
var results = new List<object>();
181+
int passCount = 0;
182+
183+
foreach (var tc in cases)
184+
{
185+
// 复用 Run 逻辑(简化版,直接内联)
186+
var definition = await _db.FlowDefinitions
187+
.FirstOrDefaultAsync(d => d.FlowKey == tc.FlowKey && d.Deleted == 0);
188+
if (definition == null || string.IsNullOrEmpty(definition.FlowContent) || definition.FlowContent == "[]")
189+
{
190+
results.Add(new { caseId = tc.Id, caseName = tc.CaseName, status = "FAILED", summary = "流程不存在或内容为空" });
191+
continue;
192+
}
193+
194+
Dictionary<string, object?> inputParams = new();
195+
if (!string.IsNullOrEmpty(tc.InputJson))
196+
{
197+
try { inputParams = JsonSerializer.Deserialize<Dictionary<string, object?>>(tc.InputJson,
198+
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new(); } catch { }
199+
}
200+
201+
var execResult = await _flowExec.RunAsync(definition, definition.FlowContent!, inputParams, "testcase");
202+
bool allPassed = execResult.Success;
203+
204+
if (execResult.Success && !string.IsNullOrEmpty(tc.AssertJson))
205+
{
206+
try
207+
{
208+
var asserts = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(tc.AssertJson,
209+
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new();
210+
foreach (var (varName, expectedVal) in asserts)
211+
{
212+
string? expected = expectedVal.ValueKind == JsonValueKind.String ? expectedVal.GetString() : expectedVal.GetRawText();
213+
object? actualObj = null;
214+
execResult.OutputData?.TryGetValue(varName, out actualObj);
215+
string? actual = actualObj?.ToString();
216+
if (actual != expected) { allPassed = false; break; }
217+
}
218+
}
219+
catch { allPassed = false; }
220+
}
221+
222+
var status = allPassed ? "SUCCESS" : "FAILED";
223+
var summary = execResult.Success ? (allPassed ? "断言全部通过" : "断言失败") : $"执行失败: {execResult.ErrorMessage}";
224+
if (allPassed) passCount++;
225+
226+
tc.LastRunStatus = status;
227+
tc.LastRunTime = DateTime.Now.ToString("o");
228+
tc.LastRunResult = summary;
229+
tc.UpdatedAt = DateTime.Now.ToString("o");
230+
results.Add(new { caseId = tc.Id, caseName = tc.CaseName, status, summary });
231+
}
232+
await _db.SaveChangesAsync();
233+
234+
return ApiResult.Success(new
235+
{
236+
total = cases.Count,
237+
passCount,
238+
failCount = cases.Count - passCount,
239+
results
240+
});
241+
}
242+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace Juggle.Domain.Entities;
2+
3+
/// <summary>流程测试用例</summary>
4+
public class FlowTestCaseEntity : BaseEntity
5+
{
6+
public string FlowKey { get; set; } = "";
7+
public string CaseName { get; set; } = "";
8+
public string? InputJson { get; set; } // 入参 JSON
9+
public string? AssertJson { get; set; } // 断言 JSON:{"varName": "expectedValue"}
10+
public string? LastRunStatus { get; set; } // SUCCESS/FAILED/PENDING
11+
public string? LastRunTime { get; set; }
12+
public string? LastRunResult { get; set; } // 最近执行结果摘要
13+
public string? Remark { get; set; }
14+
}

0 commit comments

Comments
 (0)