Skip to content

Commit 15622d8

Browse files
committed
feat: support to output the HTML report
1 parent 1fd4586 commit 15622d8

19 files changed

+323
-87
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ collector-coverage.out
55
dist/
66
.vscode/launch.json
77
sample.yaml
8+
.DS_Store

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This is a API testing tool.
88

99
## Features
1010

11+
* Multiple test report formats: Markdown, HTML, Stdout
1112
* Response Body fields equation check
1213
* Response Body [eval](https://expr.medv.io/)
1314
* Verify the Kubernetes resources

cmd/run.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func newDefaultRunOption() *runOption {
4646
}
4747
}
4848

49-
func newDiskCardRunOption() *runOption {
49+
func newDiscardRunOption() *runOption {
5050
return &runOption{
5151
reporter: runner.NewDiscardTestReporter(),
5252
reportWriter: runner.NewDiscardResultWriter(),
@@ -74,7 +74,7 @@ See also https://github.com/LinuxSuRen/api-testing/tree/master/sample`,
7474
flags.DurationVarP(&opt.duration, "duration", "", 0, "Running duration")
7575
flags.DurationVarP(&opt.requestTimeout, "request-timeout", "", time.Minute, "Timeout for per request")
7676
flags.BoolVarP(&opt.requestIgnoreError, "request-ignore-error", "", false, "Indicate if ignore the request error")
77-
flags.StringVarP(&opt.report, "report", "", "", "The type of target report. Supported: markdown, md, discard, std")
77+
flags.StringVarP(&opt.report, "report", "", "", "The type of target report. Supported: markdown, md, html, discard, std")
7878
flags.StringVarP(&opt.reportFile, "report-file", "", "", "The file path of the report")
7979
flags.BoolVarP(&opt.reportIgnore, "report-ignore", "", false, "Indicate if ignore the report output")
8080
flags.Int64VarP(&opt.thread, "thread", "", 1, "Threads of the execution")
@@ -98,6 +98,8 @@ func (o *runOption) preRunE(cmd *cobra.Command, args []string) (err error) {
9898
switch o.report {
9999
case "markdown", "md":
100100
o.reportWriter = runner.NewMarkdownResultWriter(writer)
101+
case "html":
102+
o.reportWriter = runner.NewHTMLResultWriter(writer)
101103
case "discard":
102104
o.reportWriter = runner.NewDiscardResultWriter()
103105
case "", "std":
@@ -178,7 +180,7 @@ func (o *runOption) runSuiteWithDuration(suite string) (err error) {
178180
defer sem.Release(1)
179181
defer wait.Done()
180182
defer func() {
181-
fmt.Println("routing end with", time.Now().Sub(now))
183+
fmt.Println("routing end with", time.Since(now))
182184
}()
183185

184186
dataContext := getDefaultContext()

cmd/run_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func TestRunSuite(t *testing.T) {
5353
defer gock.Clean()
5454
util.MakeSureNotNil(tt.prepare)()
5555
ctx := getDefaultContext()
56-
opt := newDiskCardRunOption()
56+
opt := newDiscardRunOption()
5757
opt.requestTimeout = 30 * time.Second
5858
opt.limiter = limit.NewDefaultRateLimiter(0, 0)
5959
stopSingal := make(chan struct{}, 1)

pkg/render/template.go

+11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package render
22

33
import (
44
"bytes"
5+
"fmt"
56
"html/template"
7+
"io"
68
"strings"
79

810
"github.com/Masterminds/sprig/v3"
@@ -31,3 +33,12 @@ func FuncMap() template.FuncMap {
3133
}
3234
return funcs
3335
}
36+
37+
// RenderThenPrint renders the template then prints the result
38+
func RenderThenPrint(name, text string, ctx interface{}, w io.Writer) (err error) {
39+
var report string
40+
if report, err = Render(name, text, ctx); err == nil {
41+
fmt.Fprint(w, report)
42+
}
43+
return
44+
}

pkg/render/template_test.go

+30
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package render
22

33
import (
4+
"bytes"
45
"testing"
56

67
"github.com/stretchr/testify/assert"
@@ -56,3 +57,32 @@ func TestRender(t *testing.T) {
5657
})
5758
}
5859
}
60+
61+
func TestRenderThenPrint(t *testing.T) {
62+
tests := []struct {
63+
name string
64+
tplText string
65+
ctx interface{}
66+
buf *bytes.Buffer
67+
expect string
68+
}{{
69+
name: "simple",
70+
tplText: `{{max 1 2 3}}`,
71+
ctx: nil,
72+
buf: new(bytes.Buffer),
73+
expect: `3`,
74+
}, {
75+
name: "with a map as context",
76+
tplText: `{{.name}}`,
77+
ctx: map[string]string{"name": "linuxsuren"},
78+
buf: new(bytes.Buffer),
79+
expect: "linuxsuren",
80+
}}
81+
for _, tt := range tests {
82+
t.Run(tt.name, func(t *testing.T) {
83+
err := RenderThenPrint(tt.name, tt.tplText, tt.ctx, tt.buf)
84+
assert.NoError(t, err)
85+
assert.Equal(t, tt.expect, tt.buf.String())
86+
})
87+
}
88+
}

pkg/runner/data/html.html

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<!DOCTYPE>
2+
<html lang="zh">
3+
<head>
4+
<title>API Testing Report</title>
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<style type="text/css">
7+
[leading-7=""] {
8+
line-height: 1.75rem;
9+
}
10+
.text-center, [text-center=""] {
11+
text-align: center;
12+
}
13+
footer {
14+
position: fixed;
15+
bottom: 0;
16+
width: 100%;
17+
height: 60px;
18+
}
19+
</style>
20+
</head>
21+
<body>
22+
<table>
23+
<caption>API Testing Report</caption>
24+
<tr><th>API</th><th>Average</th><th>Max</th><th>Min</th><th>Count</th><th>Error</th></tr>
25+
{{- range $val := .}}
26+
<tr><td>{{$val.API}}</td><td>{{$val.Average}}</td><td>{{$val.Max}}</td><td>{{$val.Min}}</td><td>{{$val.Count}}</td><td>{{$val.Error}}</td></tr>
27+
{{- end}}
28+
</table>
29+
<footer text-center="" leading-7="">
30+
<p text-sm=""><a href="https://github.com/LinuxSuRen/api-testing" target="_blank" rel="noopener">Powered by API Testing</a></p>
31+
</footer>
32+
</body>
33+
</html>

pkg/runner/reporter_memory.go

+5-2
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ func (r *memoryTestReporter) ExportAllReportResults() (result ReportResultSlice,
6161
if record.EndTime.After(item.Last) {
6262
item.Last = record.EndTime
6363
}
64-
if record.BeginTime.Before(item.First) {
65-
item.First = record.BeginTime
64+
if record.ErrorCount() > 0 && record.Body != "" {
65+
item.LastErrorMessage = record.Body
6666
}
6767
} else {
6868
resultWithTotal[api] = &ReportResultWithTotal{
@@ -77,6 +77,9 @@ func (r *memoryTestReporter) ExportAllReportResults() (result ReportResultSlice,
7777
Last: record.EndTime,
7878
Total: duration,
7979
}
80+
if record.ErrorCount() > 0 {
81+
resultWithTotal[api].LastErrorMessage = record.Body
82+
}
8083
}
8184
}
8285

pkg/runner/reporter_memory_test.go

+27-6
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func TestExportAllReportResults(t *testing.T) {
3838
BeginTime: now,
3939
EndTime: now.Add(time.Second * 4),
4040
Error: errors.New("fake"),
41+
Body: "fake",
4142
}, {
4243
API: urlFoo,
4344
Method: http.MethodGet,
@@ -62,12 +63,13 @@ func TestExportAllReportResults(t *testing.T) {
6263
Count: 1,
6364
Error: 0,
6465
}, {
65-
API: "GET http://foo",
66-
Average: time.Second * 3,
67-
Max: time.Second * 4,
68-
Min: time.Second * 2,
69-
Count: 3,
70-
Error: 1,
66+
API: "GET http://foo",
67+
Average: time.Second * 3,
68+
Max: time.Second * 4,
69+
Min: time.Second * 2,
70+
Count: 3,
71+
Error: 1,
72+
LastErrorMessage: "fake",
7173
}, {
7274
API: "GET http://bar",
7375
Average: time.Second,
@@ -77,6 +79,25 @@ func TestExportAllReportResults(t *testing.T) {
7779
Count: 1,
7880
Error: 0,
7981
}},
82+
}, {
83+
name: "first record has error",
84+
records: []*runner.ReportRecord{{
85+
API: urlFoo,
86+
Method: http.MethodGet,
87+
BeginTime: now,
88+
EndTime: now.Add(time.Second * 4),
89+
Error: errors.New("fake"),
90+
Body: "fake",
91+
}},
92+
expect: runner.ReportResultSlice{{
93+
API: "GET http://foo",
94+
Average: time.Second * 4,
95+
Max: time.Second * 4,
96+
Min: time.Second * 4,
97+
Count: 1,
98+
Error: 1,
99+
LastErrorMessage: "fake",
100+
}},
80101
}}
81102
for _, tt := range tests {
82103
t.Run(tt.name, func(t *testing.T) {

pkg/runner/simple.go

+8-11
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,14 @@ func NewReportRecord() *ReportRecord {
111111

112112
// ReportResult represents the report result of a set of the same API requests
113113
type ReportResult struct {
114-
API string
115-
Count int
116-
Average time.Duration
117-
Max time.Duration
118-
Min time.Duration
119-
QPS int
120-
Error int
114+
API string
115+
Count int
116+
Average time.Duration
117+
Max time.Duration
118+
Min time.Duration
119+
QPS int
120+
Error int
121+
LastErrorMessage string
121122
}
122123

123124
// ReportResultSlice is the alias type of ReportResult slice
@@ -202,10 +203,6 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte
202203
}(record)
203204

204205
defer func() {
205-
if testcase.Clean.CleanPrepare {
206-
err = r.doCleanPrepare(testcase)
207-
}
208-
209206
if err == nil {
210207
err = runJob(testcase.After)
211208
}

pkg/runner/simple_test.go

-3
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,6 @@ func TestTestCase(t *testing.T) {
7272
Before: atest.Job{
7373
Items: []string{"sleep(1)"},
7474
},
75-
Clean: atest.Clean{
76-
CleanPrepare: true,
77-
},
7875
},
7976
execer: fakeruntime.FakeExecer{},
8077
prepare: func() {

pkg/runner/testdata/report.html

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<!DOCTYPE>
2+
<html lang="zh">
3+
<head>
4+
<title>API Testing Report</title>
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<style type="text/css">
7+
[leading-7=""] {
8+
line-height: 1.75rem;
9+
}
10+
.text-center, [text-center=""] {
11+
text-align: center;
12+
}
13+
footer {
14+
position: fixed;
15+
bottom: 0;
16+
width: 100%;
17+
height: 60px;
18+
}
19+
</style>
20+
</head>
21+
<body>
22+
<table>
23+
<caption>API Testing Report</caption>
24+
<tr><th>API</th><th>Average</th><th>Max</th><th>Min</th><th>Count</th><th>Error</th></tr>
25+
<tr><td>/foo</td><td>3ns</td><td>3ns</td><td>3ns</td><td>1</td><td>0</td></tr>
26+
</table>
27+
<footer text-center="" leading-7="">
28+
<p text-sm=""><a href="https://github.com/LinuxSuRen/api-testing" target="_blank" rel="noopener">Powered by API Testing</a></p>
29+
</footer>
30+
</body>
31+
</html>

pkg/runner/writer_html.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package runner
2+
3+
import (
4+
_ "embed"
5+
"io"
6+
7+
"github.com/linuxsuren/api-testing/pkg/render"
8+
)
9+
10+
type htmlResultWriter struct {
11+
writer io.Writer
12+
}
13+
14+
// NewHTMLResultWriter creates a new htmlResultWriter
15+
func NewHTMLResultWriter(writer io.Writer) ReportResultWriter {
16+
return &htmlResultWriter{writer: writer}
17+
}
18+
19+
// Output writes the HTML base report to target writer
20+
func (w *htmlResultWriter) Output(result []ReportResult) (err error) {
21+
return render.RenderThenPrint("html-report", htmlReport, result, w.writer)
22+
}
23+
24+
//go:embed data/html.html
25+
var htmlReport string

pkg/runner/writer_html_test.go

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package runner_test
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
_ "embed"
8+
9+
"github.com/linuxsuren/api-testing/pkg/runner"
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestHTMLResultWriter(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
buf *bytes.Buffer
17+
results []runner.ReportResult
18+
expect string
19+
}{{
20+
name: "simple",
21+
buf: new(bytes.Buffer),
22+
results: []runner.ReportResult{{
23+
API: "/foo",
24+
Max: 3,
25+
Min: 3,
26+
Average: 3,
27+
Error: 0,
28+
Count: 1,
29+
}},
30+
expect: htmlReportExpect,
31+
}}
32+
for _, tt := range tests {
33+
t.Run(tt.name, func(t *testing.T) {
34+
w := runner.NewHTMLResultWriter(tt.buf)
35+
err := w.Output(tt.results)
36+
assert.NoError(t, err)
37+
assert.Equal(t, tt.expect, tt.buf.String())
38+
})
39+
}
40+
}
41+
42+
//go:embed testdata/report.html
43+
var htmlReportExpect string

0 commit comments

Comments
 (0)