feat(tests): 增强测试框架的 SQL 日志记录与覆盖率报告
- 在 ApiCase 中添加 SQL 日志记录功能,失败时输出日志以便于调试 - 更新 TestApp 以支持测试模式下的 SQL 日志缓冲,成功与失败的测试均可记录日志 - 改进 PrintCoverage 方法,优化覆盖率报告的输出逻辑,支持部分运行的接口显示 - 增加 Note 备注功能,提升测试用例的可读性与文档化效果 - 更新相关文档,详细说明新功能的使用场景与示例
This commit is contained in:
+92
-16
@@ -51,8 +51,13 @@ func inferParamsFromCases(cases []swaggerTestCase) []paramSpec {
|
||||
return -1
|
||||
}
|
||||
if s, ok := resp["status"]; ok {
|
||||
if f, ok := s.(float64); ok {
|
||||
return int(f)
|
||||
switch v := s.(type) {
|
||||
case float64:
|
||||
return int(v)
|
||||
case int64:
|
||||
return int(v)
|
||||
case int:
|
||||
return v
|
||||
}
|
||||
}
|
||||
return -1
|
||||
@@ -60,13 +65,15 @@ func inferParamsFromCases(cases []swaggerTestCase) []paramSpec {
|
||||
|
||||
inferType := func(v interface{}) string {
|
||||
switch v.(type) {
|
||||
case float64:
|
||||
case int, int8, int16, int32, int64,
|
||||
uint, uint8, uint16, uint32, uint64,
|
||||
float32, float64:
|
||||
return "number"
|
||||
case bool:
|
||||
return "boolean"
|
||||
case []interface{}:
|
||||
case []interface{}, Slice:
|
||||
return "array"
|
||||
case map[string]interface{}:
|
||||
case map[string]interface{}, Map:
|
||||
return "object"
|
||||
default:
|
||||
return "string"
|
||||
@@ -95,6 +102,7 @@ func inferParamsFromCases(cases []swaggerTestCase) []paramSpec {
|
||||
}
|
||||
|
||||
// 第一轮:收集所有参数 key、类型和示例值
|
||||
// 成功用例(status=0)优先级最高,覆盖 in/typ/example,代表真实请求方式和数据类型
|
||||
for _, c := range cases {
|
||||
st := getStatus(c.Response)
|
||||
for k, v := range c.Query {
|
||||
@@ -102,10 +110,14 @@ func inferParamsFromCases(cases []swaggerTestCase) []paramSpec {
|
||||
params[k] = ¶mInfo{in: "query"}
|
||||
}
|
||||
p := params[k]
|
||||
if st == 0 {
|
||||
p.in = "query"
|
||||
}
|
||||
if p.typ == "" && sv(v) != "" {
|
||||
p.typ = inferType(v)
|
||||
}
|
||||
if p.example == nil && st == 0 && sv(v) != "" {
|
||||
if st == 0 && sv(v) != "" {
|
||||
p.typ = inferType(v)
|
||||
p.example = v
|
||||
}
|
||||
}
|
||||
@@ -114,10 +126,14 @@ func inferParamsFromCases(cases []swaggerTestCase) []paramSpec {
|
||||
params[k] = ¶mInfo{in: "form"}
|
||||
}
|
||||
p := params[k]
|
||||
if st == 0 {
|
||||
p.in = "form"
|
||||
}
|
||||
if p.typ == "" && sv(v) != "" {
|
||||
p.typ = inferType(v)
|
||||
}
|
||||
if p.example == nil && st == 0 && sv(v) != "" {
|
||||
if st == 0 && sv(v) != "" {
|
||||
p.typ = inferType(v)
|
||||
p.example = v
|
||||
}
|
||||
}
|
||||
@@ -127,40 +143,76 @@ func inferParamsFromCases(cases []swaggerTestCase) []paramSpec {
|
||||
params[k] = ¶mInfo{in: "json"}
|
||||
}
|
||||
p := params[k]
|
||||
if st == 0 {
|
||||
p.in = "json"
|
||||
}
|
||||
if p.typ == "" && sv(v) != "" {
|
||||
p.typ = inferType(v)
|
||||
}
|
||||
if p.example == nil && st == 0 && sv(v) != "" {
|
||||
if st == 0 && sv(v) != "" {
|
||||
p.typ = inferType(v)
|
||||
p.example = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 第二轮:标记必填 — 参数存在但值为空 且 status==3
|
||||
// 第二轮:标记必填 — 参数存在但值为空 且 status==3(不限制 in 类型,因为错误用例可能用不同传参方式)
|
||||
for _, c := range cases {
|
||||
if getStatus(c.Response) != 3 {
|
||||
continue
|
||||
}
|
||||
for k, v := range c.Query {
|
||||
if p, ok := params[k]; ok && p.in == "query" && sv(v) == "" {
|
||||
if p, ok := params[k]; ok && sv(v) == "" {
|
||||
p.required = true
|
||||
}
|
||||
}
|
||||
for k, v := range c.Form {
|
||||
if p, ok := params[k]; ok && p.in == "form" && sv(v) == "" {
|
||||
if p, ok := params[k]; ok && sv(v) == "" {
|
||||
p.required = true
|
||||
}
|
||||
}
|
||||
if jmap := toStrMap(c.Json); jmap != nil {
|
||||
for k, v := range jmap {
|
||||
if p, ok := params[k]; ok && p.in == "json" && sv(v) == "" {
|
||||
if p, ok := params[k]; ok && sv(v) == "" {
|
||||
p.required = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 第三轮:标记必填 — 字段在 status==3 用例中缺失,但在下一个用例中出现
|
||||
collectKeys := func(c swaggerTestCase) map[string]bool {
|
||||
keys := map[string]bool{}
|
||||
for k := range c.Query {
|
||||
keys[k] = true
|
||||
}
|
||||
for k := range c.Form {
|
||||
keys[k] = true
|
||||
}
|
||||
if jm := toStrMap(c.Json); jm != nil {
|
||||
for k := range jm {
|
||||
keys[k] = true
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
for i := 0; i < len(cases)-1; i++ {
|
||||
if getStatus(cases[i].Response) != 3 {
|
||||
continue
|
||||
}
|
||||
curKeys := collectKeys(cases[i])
|
||||
nextKeys := collectKeys(cases[i+1])
|
||||
for k := range nextKeys {
|
||||
if !curKeys[k] {
|
||||
if p, ok := params[k]; ok {
|
||||
p.required = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
result := make([]paramSpec, 0, len(params))
|
||||
for name, p := range params {
|
||||
result = append(result, paramSpec{
|
||||
@@ -509,6 +561,12 @@ pre.j.resp-j{max-height:calc(100vh - 280px);min-height:120px}
|
||||
.bd-bd textarea{width:100%;min-height:36px;max-height:40vh;padding:5px 8px;border:1px solid var(--border);border-radius:var(--r);background:var(--bg2);color:var(--fg);font-size:12px;font-family:Consolas,Monaco,monospace;resize:vertical;outline:none;overflow-y:auto}
|
||||
.bd-bd textarea:focus{border-color:var(--accent)}
|
||||
.req-star{color:var(--red);font-size:10px;font-weight:700;margin-right:2px;flex-shrink:0;line-height:1.2;align-self:center}
|
||||
.jp-panel{margin-bottom:6px;border:1px solid var(--border);border-radius:var(--r);overflow:hidden}
|
||||
.jp-hd{padding:3px 8px;background:var(--bg2);cursor:pointer;display:flex;align-items:center;font-size:11px;color:var(--fg2);user-select:none}
|
||||
.jp-bd{padding:6px 8px;display:flex;flex-wrap:wrap;gap:4px;border-top:1px solid var(--border)}
|
||||
.jp-tag{font-size:11px;padding:1px 6px;background:var(--bg3);border:1px solid var(--border);border-radius:var(--r);color:var(--fg2);display:inline-flex;align-items:center;gap:2px}
|
||||
.jp-req{color:var(--accent);border-color:rgba(79,195,247,.3)}
|
||||
.jp-type{color:#555;font-size:10px}
|
||||
</style></head><body>
|
||||
|
||||
<div id="side">
|
||||
@@ -680,7 +738,24 @@ function buildLeft(e){
|
||||
l+='<div class="bd-hd'+(defBody==='form'?' on':'')+'" data-t="form" onclick="togBody(\'form\')"><span class="ar'+(defBody==='form'?' o':'')+'">▶</span>表单 (Form)<button class="addbtn" onclick="event.stopPropagation();setAcc(\'form\');addKV(\'f-rows\')">+ 添加</button></div>';
|
||||
l+='<div id="bt-form" class="bd-bd'+(defBody==='form'?' on':'')+'"><div id="f-rows"><div class="kv"><input placeholder="key"><input placeholder="value"><button onclick="this.parentElement.remove()">×</button></div></div></div>';
|
||||
l+='<div class="bd-hd'+(defBody==='json'?' on':'')+'" data-t="json" onclick="togBody(\'json\')"><span class="ar'+(defBody==='json'?' o':'')+'">▶</span>JSON</div>';
|
||||
l+='<div id="bt-json" class="bd-bd'+(defBody==='json'?' on':'')+'"><textarea id="xb" oninput="autoH(this)" placeholder="'+(jsonPH?esc(jsonPH):'{"key":"value"}')+'"></textarea></div>';
|
||||
l+='<div id="bt-json" class="bd-bd'+(defBody==='json'?' on':'')+'">';
|
||||
if(e.params){
|
||||
const jp=e.params.filter(p=>p.in==='json');
|
||||
if(jp.length){
|
||||
const jpVis=localStorage.getItem('json_params_vis')!=='0';
|
||||
l+='<div class="jp-panel"><div class="jp-hd" onclick="togJP()"><span class="ar jp-ar'+(jpVis?' o':'')+'">▶</span><span>JSON 参数</span><span style="margin-left:auto;font-size:10px" class="jp-toggle">'+(jpVis?'收起':'展开')+'</span></div>';
|
||||
l+='<div class="jp-bd"'+(jpVis?'':' style="display:none"')+'>';
|
||||
for(const p of jp){
|
||||
const star=p.required?'<span class="req-star">*</span>':'';
|
||||
const cls=p.required?'jp-tag jp-req':'jp-tag';
|
||||
l+=star+'<span class="'+cls+'">'+esc(p.name);
|
||||
if(p.type)l+=' <span class="jp-type">'+p.type+'</span>';
|
||||
l+='</span>';
|
||||
}
|
||||
l+='</div></div>';
|
||||
}
|
||||
}
|
||||
l+='<textarea id="xb" oninput="autoH(this)" placeholder="'+(jsonPH?esc(jsonPH):'{"key":"value"}')+'"></textarea></div>';
|
||||
l+='</div></div>';
|
||||
return l;
|
||||
}
|
||||
@@ -806,7 +881,7 @@ function pfFill(v){
|
||||
if(nb){if(c.note){nb.textContent='备注: '+c.note;nb.style.display='block';}else{nb.textContent='';nb.style.display='none';}}
|
||||
if(c.query){
|
||||
for(const[k,val]of Object.entries(c.query)){
|
||||
const req=C?.params?.find(p=>p.name===k&&p.in==='query')?.required||false;
|
||||
const req=C?.params?.find(p=>p.name===k)?.required||false;
|
||||
addKVReq('q-rows',k,String(val),req);
|
||||
}
|
||||
}
|
||||
@@ -814,7 +889,7 @@ function pfFill(v){
|
||||
if(c.json){
|
||||
setAcc('json');const el=document.getElementById('xb');
|
||||
if(el){
|
||||
const rq=new Set((C?.params||[]).filter(p=>p.required&&p.in==='json').map(p=>p.name));
|
||||
const rq=new Set((C?.params||[]).filter(p=>p.required).map(p=>p.name));
|
||||
let obj=c.json;
|
||||
if(rq.size&&typeof obj==='object'&&obj!==null&&!Array.isArray(obj)){
|
||||
const sorted={};
|
||||
@@ -828,7 +903,7 @@ function pfFill(v){
|
||||
if(fr){
|
||||
fr.innerHTML='';
|
||||
for(const[k,val]of Object.entries(c.form)){
|
||||
const req=C?.params?.find(p=>p.name===k&&p.in==='form')?.required||false;
|
||||
const req=C?.params?.find(p=>p.name===k)?.required||false;
|
||||
addKVReq('f-rows',k,String(val),req);
|
||||
}
|
||||
addKV('f-rows');
|
||||
@@ -904,6 +979,7 @@ function copyEl(id){navigator.clipboard.writeText(document.getElementById(id).te
|
||||
function cpNext(btn){let el=btn.closest('h5').nextElementSibling;let t='';while(el&&el.tagName!=='H5'){if(el.tagName==='PRE')t+=(t?'\n':'')+el.textContent;el=el.nextElementSibling;}navigator.clipboard.writeText(t).then(()=>{btn.textContent='已复制';setTimeout(()=>{btn.textContent='复制'},1200);});}
|
||||
function getExpVis(){const v=localStorage.getItem('expect_vis');return v===null||v==='1';}
|
||||
function toggleExpect(){const vis=getExpVis();const nv=!vis;localStorage.setItem('expect_vis',nv?'1':'0');document.querySelectorAll('.resp-wrap').forEach(w=>{w.classList.toggle('exp-on',nv);});document.querySelectorAll('.exp-btn').forEach(b=>{b.textContent=nv?'收起 预期响应':'展开 预期响应';});}
|
||||
function togJP(){const bd=document.querySelector('.jp-bd');const ar=document.querySelector('.jp-ar');const tg=document.querySelector('.jp-toggle');if(!bd)return;const vis=bd.style.display!=='none';bd.style.display=vis?'none':'flex';if(ar)ar.classList.toggle('o',!vis);if(tg)tg.textContent=vis?'展开':'收起';localStorage.setItem('json_params_vis',vis?'0':'1');}
|
||||
function fj(o){return esc(JSON.stringify(o,null,2));}
|
||||
function fjp(o,params){
|
||||
if(!params||!params.length||typeof o!=='object'||o===null||Array.isArray(o))return fj(o);
|
||||
|
||||
Reference in New Issue
Block a user