diff --git a/README.md b/README.md index 4b58310..5910b1d 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,8 @@ autotest extract -x "//title" -j '[ } ]' ``` +## Test Report + ## TODO * [x] 1) support utilizing the script language Lua to ascertain the conformity of HTTP responses with expectations. diff --git a/README_zh.md b/README_zh.md index 0eca281..8ca5628 100644 --- a/README_zh.md +++ b/README_zh.md @@ -92,6 +92,8 @@ autotest extract -x "//title" -j '[ } ]' ``` +## 测试报告 + ## TODO * [x] 1) 支持使用脚本语言Lua判断HTTP response是否符合预期 diff --git a/config_files/autotest.yml b/config_files/autotest.yml index dbb9e10..c1793a0 100644 --- a/config_files/autotest.yml +++ b/config_files/autotest.yml @@ -4,7 +4,7 @@ global: worker_num: 5 # default: true ignore_testcase_fail: true - debug: true + debug: false # Timeout setting for each request request_timeout: 5s @@ -20,8 +20,8 @@ global: http_rule_files: - "./config_files/my_http_api.yml" -grpc_rule_files: - - "./config_files/my_grpc_api.yml" +#grpc_rule_files: +# - "./config_files/my_grpc_api.yml" diff --git a/go.mod b/go.mod index 640d77f..550d00e 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/golang/protobuf v1.5.4 github.com/jhump/protoreflect v1.16.0 github.com/lianggaoqiang/progress v0.0.1 - github.com/spf13/cast v1.6.0 + github.com/spf13/cast v1.7.0 github.com/stretchr/testify v1.9.0 github.com/urfave/cli/v3 v3.0.0-alpha9 github.com/vearne/executor v0.0.3 diff --git a/go.sum b/go.sum index 95bfcfe..68fe9f7 100644 --- a/go.sum +++ b/go.sum @@ -61,6 +61,8 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/img/result_html.jpg b/img/result_html.jpg new file mode 100644 index 0000000..614cf05 Binary files /dev/null and b/img/result_html.jpg differ diff --git a/internal/command/http_automate.go b/internal/command/http_automate.go index 676f22f..52a3d18 100644 --- a/internal/command/http_automate.go +++ b/internal/command/http_automate.go @@ -2,6 +2,8 @@ package command import ( "context" + "embed" + "fmt" "github.com/lianggaoqiang/progress" "github.com/vearne/autotest/internal/config" "github.com/vearne/autotest/internal/model" @@ -11,6 +13,8 @@ import ( slog "github.com/vearne/simplelog" "github.com/vearne/zaplog" "go.uber.org/zap" + "html/template" + "os" "path/filepath" "sort" "strconv" @@ -18,12 +22,23 @@ import ( "time" ) +//go:embed template/*.tpl +var mytpl embed.FS + type ResultInfo struct { Total int SuccessCount int FailedCount int } +type CaseShow struct { + ID uint64 + Description string + State string + Reason string + Link string +} + func HttpAutomateTest(httpTestCases map[string][]*config.TestCaseHttp) { total := 0 for _, testcases := range httpTestCases { @@ -54,12 +69,12 @@ func HttpAutomateTest(httpTestCases map[string][]*config.TestCaseHttp) { slog.Info("HttpTestCases, total:%v, finishCount:%v, successCount:%v, failedCount:%v", total, finishCount, successCount, failedCount) // generate report file - GenReportFileHttp(filePath, tcResultList) + GenReportFileHttp(filePath, tcResultList, info) } slog.Info("[end]HttpTestCases, total:%v, cost:%v", total, time.Since(begin)) } -func GenReportFileHttp(testCasefilePath string, tcResultList []HttpTestCaseResult) { +func GenReportFileHttp(testCasefilePath string, tcResultList []HttpTestCaseResult, info *ResultInfo) { filename := filepath.Base(testCasefilePath) name := strings.TrimSuffix(filename, filepath.Ext(filename)) filename = name + ".csv" @@ -69,8 +84,9 @@ func GenReportFileHttp(testCasefilePath string, tcResultList []HttpTestCaseResul sort.Slice(tcResultList, func(i, j int) bool { return tcResultList[i].ID < tcResultList[j].ID }) + // 1. csv file var records [][]string - records = append(records, []string{"id", "desc", "state", "reason"}) + records = append(records, []string{"id", "description", "state", "reason"}) for _, item := range tcResultList { reasonStr := item.Reason.String() if item.Reason == model.ReasonSuccess { @@ -80,6 +96,74 @@ func GenReportFileHttp(testCasefilePath string, tcResultList []HttpTestCaseResul item.Desc, item.State.String(), reasonStr}) } util.WriterCSV(reportPath, records) + // 2. html file + dirName := util.MD5(reportDirPath + name) + + var caseResults []CaseShow + for _, item := range tcResultList { + caseResults = append(caseResults, CaseShow{ID: item.ID, Description: item.Desc, + State: item.State.String(), Reason: item.Reason.String(), + Link: fmt.Sprintf("./%v/%v.html", dirName, item.ID)}) + } + + obj := map[string]any{ + "info": info, + "tcResultList": caseResults, + } + // index file + err := RenderTpl(mytpl, "template/index.tpl", obj, filepath.Join(reportDirPath, name+".html")) + if err != nil { + slog.Error("RenderTpl, %v", err) + return + } + + for _, item := range tcResultList { + data := map[string]any{ + "Error": item.Error, + "reqDetail": item.ReqDetail(), + "respDetail": item.RespDetail(), + } + err := RenderTpl(mytpl, "template/case.tpl", data, + filepath.Join(reportDirPath, dirName, strconv.Itoa(int(item.ID))+".html")) + if err != nil { + slog.Error("RenderTpl, %v", err) + return + } + } +} + +func RenderTpl(fs embed.FS, key string, obj map[string]any, targetPath string) error { + data, err := fs.ReadFile(key) + if err != nil { + slog.Error("mytpl.ReadFile, %v", err) + return err + } + t, err := template.New("index").Parse(string(data)) + if err != nil { + slog.Error("template Parse, %v", err) + return err + } + dirPath := filepath.Dir(targetPath) + if !pathExists(dirPath) { + err = os.Mkdir(dirPath, 0755) + if err != nil { + return err + } + } + + file, err := os.Create(targetPath) + if err != nil { + slog.Error("Create file, %v", err) + return err + } + defer file.Close() + return t.Execute(file, obj) +} + +func pathExists(path string) bool { + _, err := os.Stat(path) + // os.IsNotExist 判断错误是否为文件或目录不存在 + return !os.IsNotExist(err) } func HandleSingleFileHttp(workerNum int, filePath string) (*ResultInfo, []HttpTestCaseResult) { diff --git a/internal/command/http_call.go b/internal/command/http_call.go index 4a54581..bc6acef 100644 --- a/internal/command/http_call.go +++ b/internal/command/http_call.go @@ -2,6 +2,7 @@ package command import ( "context" + "fmt" "github.com/go-resty/resty/v2" "github.com/vearne/autotest/internal/config" "github.com/vearne/autotest/internal/model" @@ -9,6 +10,7 @@ import ( "github.com/vearne/executor" "github.com/vearne/zaplog" "go.uber.org/zap" + "net/url" "strings" "time" ) @@ -22,6 +24,66 @@ type HttpTestCaseResult struct { Request config.RequestHttp TestCase *config.TestCaseHttp KeyValues map[string]any + Error error + Response *resty.Response +} + +/* + POST /api/order/subscribe HTTP/1.1 + HOST: localhost:8080 + HEADERS: + Content-Type: application/json + User-Agent: go-resty/2.12.0 (https://github.com/go-resty/resty) + BODY: + { + "title": "book3_title", + "author": "book3_author" + } +*/ + +func (t *HttpTestCaseResult) ReqDetail() string { + var builder strings.Builder + builder.WriteString(fmt.Sprintf("%v %v\n", strings.ToUpper(t.Request.Method), t.Request.URL)) + u, _ := url.Parse(t.Request.URL) + builder.WriteString(fmt.Sprintf("HOST: %v\n", u.Host)) + builder.WriteString("HEADERS:\n") + for _, item := range t.Request.Headers { + builder.WriteString(fmt.Sprintf("%v\n", item)) + } + builder.WriteString("BODY:\n") + builder.WriteString(fmt.Sprintf("%v\n", t.Request.Body)) + return builder.String() +} + +/* + STATUS : 200 OK + PROTO : HTTP/1.1 + RECEIVED AT : 2024-10-17T17:05:05.156315+08:00 + TIME DURATION: 38.354958ms + HEADERS : + Content-Length: 17 + Content-Type: application/json; charset=utf-8 + Date: Thu, 17 Oct 2024 09:05:05 GMT + BODY : + { + "code": "E000" + } +*/ + +func (t *HttpTestCaseResult) RespDetail() string { + if t.Response == nil { + return "" + } + + var builder strings.Builder + builder.WriteString(fmt.Sprintf("STATUS: %v\n", t.Response.Status())) + builder.WriteString("HEADERS:\n") + for key, values := range t.Response.Header() { + builder.WriteString(fmt.Sprintf("%v: %v\n", key, strings.Join(values, ","))) + } + builder.WriteString("BODY:\n") + builder.WriteString(fmt.Sprintf("%v\n", t.Response.String())) + return builder.String() } type HttpTestCallable struct { @@ -40,6 +102,8 @@ func (m *HttpTestCallable) Call(ctx context.Context) *executor.GPResult { Reason: model.ReasonSuccess, TestCase: m.testcase, KeyValues: map[string]any{}, + Error: nil, + Response: nil, } // 1. Check other test cases of dependencies @@ -75,6 +139,7 @@ func (m *HttpTestCallable) Call(ctx context.Context) *executor.GPResult { if err != nil { tcResult.State = model.StateFailed tcResult.Reason = model.ReasonTemplateRenderError + tcResult.Error = err r.Value = tcResult r.Err = err return &r @@ -119,11 +184,14 @@ func (m *HttpTestCallable) Call(ctx context.Context) *executor.GPResult { ) tcResult.State = model.StateFailed tcResult.Reason = model.ReasonRequestFailed + tcResult.Error = err r.Value = tcResult r.Err = err return &r } + tcResult.Response = out + // 6. export if m.testcase.Export != nil { exportConfig := m.testcase.Export diff --git a/internal/command/template/case.tpl b/internal/command/template/case.tpl new file mode 100644 index 0000000..caf64bc --- /dev/null +++ b/internal/command/template/case.tpl @@ -0,0 +1,32 @@ + + +
+ + +{{ .reqDetail }}+
{{ .respDetail }}+ {{end}} +
id | +description | +state | +reason | +content | +
---|---|---|---|---|
{{ $element.ID }} | +{{ $element.Description }} | +{{ $element.State }} | +{{ $element.Reason }} | ++ {{ if ne $element.Reason "ReasonDependentItemFailed" }} + View Details + {{ end }} + | +