diff --git a/.gitignore b/.gitignore
index e18503c..4f6e899 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ collector-coverage.out
dist/
.vscode/launch.json
sample.yaml
+.DS_Store
diff --git a/README.md b/README.md
index 657599f..78e2cad 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,7 @@ This is a API testing tool.
## Features
+* Multiple test report formats: Markdown, HTML, Stdout
* Response Body fields equation check
* Response Body [eval](https://expr.medv.io/)
* Verify the Kubernetes resources
diff --git a/cmd/run.go b/cmd/run.go
index 6f3a75b..56a4403 100644
--- a/cmd/run.go
+++ b/cmd/run.go
@@ -46,7 +46,7 @@ func newDefaultRunOption() *runOption {
}
}
-func newDiskCardRunOption() *runOption {
+func newDiscardRunOption() *runOption {
return &runOption{
reporter: runner.NewDiscardTestReporter(),
reportWriter: runner.NewDiscardResultWriter(),
@@ -74,7 +74,7 @@ See also https://github.com/LinuxSuRen/api-testing/tree/master/sample`,
flags.DurationVarP(&opt.duration, "duration", "", 0, "Running duration")
flags.DurationVarP(&opt.requestTimeout, "request-timeout", "", time.Minute, "Timeout for per request")
flags.BoolVarP(&opt.requestIgnoreError, "request-ignore-error", "", false, "Indicate if ignore the request error")
- flags.StringVarP(&opt.report, "report", "", "", "The type of target report. Supported: markdown, md, discard, std")
+ flags.StringVarP(&opt.report, "report", "", "", "The type of target report. Supported: markdown, md, html, discard, std")
flags.StringVarP(&opt.reportFile, "report-file", "", "", "The file path of the report")
flags.BoolVarP(&opt.reportIgnore, "report-ignore", "", false, "Indicate if ignore the report output")
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) {
switch o.report {
case "markdown", "md":
o.reportWriter = runner.NewMarkdownResultWriter(writer)
+ case "html":
+ o.reportWriter = runner.NewHTMLResultWriter(writer)
case "discard":
o.reportWriter = runner.NewDiscardResultWriter()
case "", "std":
@@ -178,7 +180,7 @@ func (o *runOption) runSuiteWithDuration(suite string) (err error) {
defer sem.Release(1)
defer wait.Done()
defer func() {
- fmt.Println("routing end with", time.Now().Sub(now))
+ fmt.Println("routing end with", time.Since(now))
}()
dataContext := getDefaultContext()
diff --git a/cmd/run_test.go b/cmd/run_test.go
index f82f9e7..516eec2 100644
--- a/cmd/run_test.go
+++ b/cmd/run_test.go
@@ -53,7 +53,7 @@ func TestRunSuite(t *testing.T) {
defer gock.Clean()
util.MakeSureNotNil(tt.prepare)()
ctx := getDefaultContext()
- opt := newDiskCardRunOption()
+ opt := newDiscardRunOption()
opt.requestTimeout = 30 * time.Second
opt.limiter = limit.NewDefaultRateLimiter(0, 0)
stopSingal := make(chan struct{}, 1)
diff --git a/pkg/render/template.go b/pkg/render/template.go
index ffbfadd..b45a6a7 100644
--- a/pkg/render/template.go
+++ b/pkg/render/template.go
@@ -2,7 +2,9 @@ package render
import (
"bytes"
+ "fmt"
"html/template"
+ "io"
"strings"
"github.com/Masterminds/sprig/v3"
@@ -31,3 +33,12 @@ func FuncMap() template.FuncMap {
}
return funcs
}
+
+// RenderThenPrint renders the template then prints the result
+func RenderThenPrint(name, text string, ctx interface{}, w io.Writer) (err error) {
+ var report string
+ if report, err = Render(name, text, ctx); err == nil {
+ fmt.Fprint(w, report)
+ }
+ return
+}
diff --git a/pkg/render/template_test.go b/pkg/render/template_test.go
index 98ed746..19adc91 100644
--- a/pkg/render/template_test.go
+++ b/pkg/render/template_test.go
@@ -1,6 +1,7 @@
package render
import (
+ "bytes"
"testing"
"github.com/stretchr/testify/assert"
@@ -56,3 +57,32 @@ func TestRender(t *testing.T) {
})
}
}
+
+func TestRenderThenPrint(t *testing.T) {
+ tests := []struct {
+ name string
+ tplText string
+ ctx interface{}
+ buf *bytes.Buffer
+ expect string
+ }{{
+ name: "simple",
+ tplText: `{{max 1 2 3}}`,
+ ctx: nil,
+ buf: new(bytes.Buffer),
+ expect: `3`,
+ }, {
+ name: "with a map as context",
+ tplText: `{{.name}}`,
+ ctx: map[string]string{"name": "linuxsuren"},
+ buf: new(bytes.Buffer),
+ expect: "linuxsuren",
+ }}
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ err := RenderThenPrint(tt.name, tt.tplText, tt.ctx, tt.buf)
+ assert.NoError(t, err)
+ assert.Equal(t, tt.expect, tt.buf.String())
+ })
+ }
+}
diff --git a/pkg/runner/data/html.html b/pkg/runner/data/html.html
new file mode 100644
index 0000000..1821316
--- /dev/null
+++ b/pkg/runner/data/html.html
@@ -0,0 +1,33 @@
+
+
+
+ API Testing Report
+
+
+
+
+
+ API Testing Report
+ API | Average | Max | Min | Count | Error |
+ {{- range $val := .}}
+ {{$val.API}} | {{$val.Average}} | {{$val.Max}} | {{$val.Min}} | {{$val.Count}} | {{$val.Error}} |
+ {{- end}}
+
+
+
+
diff --git a/pkg/runner/reporter_memory.go b/pkg/runner/reporter_memory.go
index 06e3463..2c6fbd7 100644
--- a/pkg/runner/reporter_memory.go
+++ b/pkg/runner/reporter_memory.go
@@ -58,12 +58,8 @@ func (r *memoryTestReporter) ExportAllReportResults() (result ReportResultSlice,
item.Total += duration
item.Count += 1
- if record.EndTime.After(item.Last) {
- item.Last = record.EndTime
- }
- if record.BeginTime.Before(item.First) {
- item.First = record.BeginTime
- }
+ item.Last = getLaterTime(record.EndTime, item.Last)
+ item.LastErrorMessage = getOriginalStringWhenEmpty(item.LastErrorMessage, record.GetErrorMessage())
} else {
resultWithTotal[api] = &ReportResultWithTotal{
ReportResult: ReportResult{
@@ -77,6 +73,7 @@ func (r *memoryTestReporter) ExportAllReportResults() (result ReportResultSlice,
Last: record.EndTime,
Total: duration,
}
+ resultWithTotal[api].LastErrorMessage = record.GetErrorMessage()
}
}
@@ -91,3 +88,17 @@ func (r *memoryTestReporter) ExportAllReportResults() (result ReportResultSlice,
sort.Sort(result)
return
}
+
+func getLaterTime(a, b time.Time) time.Time {
+ if a.After(b) {
+ return a
+ }
+ return b
+}
+
+func getOriginalStringWhenEmpty(a, b string) string {
+ if b == "" {
+ return a
+ }
+ return b
+}
diff --git a/pkg/runner/reporter_memory_test.go b/pkg/runner/reporter_memory_test.go
index 14932ad..c621514 100644
--- a/pkg/runner/reporter_memory_test.go
+++ b/pkg/runner/reporter_memory_test.go
@@ -38,6 +38,7 @@ func TestExportAllReportResults(t *testing.T) {
BeginTime: now,
EndTime: now.Add(time.Second * 4),
Error: errors.New("fake"),
+ Body: "fake",
}, {
API: urlFoo,
Method: http.MethodGet,
@@ -62,12 +63,13 @@ func TestExportAllReportResults(t *testing.T) {
Count: 1,
Error: 0,
}, {
- API: "GET http://foo",
- Average: time.Second * 3,
- Max: time.Second * 4,
- Min: time.Second * 2,
- Count: 3,
- Error: 1,
+ API: "GET http://foo",
+ Average: time.Second * 3,
+ Max: time.Second * 4,
+ Min: time.Second * 2,
+ Count: 3,
+ Error: 1,
+ LastErrorMessage: "fake",
}, {
API: "GET http://bar",
Average: time.Second,
@@ -77,6 +79,25 @@ func TestExportAllReportResults(t *testing.T) {
Count: 1,
Error: 0,
}},
+ }, {
+ name: "first record has error",
+ records: []*runner.ReportRecord{{
+ API: urlFoo,
+ Method: http.MethodGet,
+ BeginTime: now,
+ EndTime: now.Add(time.Second * 4),
+ Error: errors.New("fake"),
+ Body: "fake",
+ }},
+ expect: runner.ReportResultSlice{{
+ API: "GET http://foo",
+ Average: time.Second * 4,
+ Max: time.Second * 4,
+ Min: time.Second * 4,
+ Count: 1,
+ Error: 1,
+ LastErrorMessage: "fake",
+ }},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/runner/simple.go b/pkg/runner/simple.go
index f6a0da6..8006e39 100644
--- a/pkg/runner/simple.go
+++ b/pkg/runner/simple.go
@@ -102,6 +102,15 @@ func (r *ReportRecord) ErrorCount() int {
return 1
}
+// GetErrorMessage returns the error message
+func (r *ReportRecord) GetErrorMessage() string {
+ if r.ErrorCount() > 0 {
+ return r.Body
+ } else {
+ return ""
+ }
+}
+
// NewReportRecord creates a record, and set the begin time to be now
func NewReportRecord() *ReportRecord {
return &ReportRecord{
@@ -111,13 +120,14 @@ func NewReportRecord() *ReportRecord {
// ReportResult represents the report result of a set of the same API requests
type ReportResult struct {
- API string
- Count int
- Average time.Duration
- Max time.Duration
- Min time.Duration
- QPS int
- Error int
+ API string
+ Count int
+ Average time.Duration
+ Max time.Duration
+ Min time.Duration
+ QPS int
+ Error int
+ LastErrorMessage string
}
// ReportResultSlice is the alias type of ReportResult slice
@@ -202,10 +212,6 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte
}(record)
defer func() {
- if testcase.Clean.CleanPrepare {
- err = r.doCleanPrepare(testcase)
- }
-
if err == nil {
err = runJob(testcase.After)
}
diff --git a/pkg/runner/simple_test.go b/pkg/runner/simple_test.go
index 4132de5..ba9cb3e 100644
--- a/pkg/runner/simple_test.go
+++ b/pkg/runner/simple_test.go
@@ -72,9 +72,6 @@ func TestTestCase(t *testing.T) {
Before: atest.Job{
Items: []string{"sleep(1)"},
},
- Clean: atest.Clean{
- CleanPrepare: true,
- },
},
execer: fakeruntime.FakeExecer{},
prepare: func() {
diff --git a/pkg/runner/testdata/report.html b/pkg/runner/testdata/report.html
new file mode 100644
index 0000000..1686a65
--- /dev/null
+++ b/pkg/runner/testdata/report.html
@@ -0,0 +1,31 @@
+
+
+
+ API Testing Report
+
+
+
+
+
+ API Testing Report
+ API | Average | Max | Min | Count | Error |
+ /foo | 3ns | 3ns | 3ns | 1 | 0 |
+
+
+
+
\ No newline at end of file
diff --git a/pkg/runner/writer_html.go b/pkg/runner/writer_html.go
new file mode 100644
index 0000000..a1d181c
--- /dev/null
+++ b/pkg/runner/writer_html.go
@@ -0,0 +1,25 @@
+package runner
+
+import (
+ _ "embed"
+ "io"
+
+ "github.com/linuxsuren/api-testing/pkg/render"
+)
+
+type htmlResultWriter struct {
+ writer io.Writer
+}
+
+// NewHTMLResultWriter creates a new htmlResultWriter
+func NewHTMLResultWriter(writer io.Writer) ReportResultWriter {
+ return &htmlResultWriter{writer: writer}
+}
+
+// Output writes the HTML base report to target writer
+func (w *htmlResultWriter) Output(result []ReportResult) (err error) {
+ return render.RenderThenPrint("html-report", htmlReport, result, w.writer)
+}
+
+//go:embed data/html.html
+var htmlReport string
diff --git a/pkg/runner/writer_html_test.go b/pkg/runner/writer_html_test.go
new file mode 100644
index 0000000..3413554
--- /dev/null
+++ b/pkg/runner/writer_html_test.go
@@ -0,0 +1,43 @@
+package runner_test
+
+import (
+ "bytes"
+ "testing"
+
+ _ "embed"
+
+ "github.com/linuxsuren/api-testing/pkg/runner"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestHTMLResultWriter(t *testing.T) {
+ tests := []struct {
+ name string
+ buf *bytes.Buffer
+ results []runner.ReportResult
+ expect string
+ }{{
+ name: "simple",
+ buf: new(bytes.Buffer),
+ results: []runner.ReportResult{{
+ API: "/foo",
+ Max: 3,
+ Min: 3,
+ Average: 3,
+ Error: 0,
+ Count: 1,
+ }},
+ expect: htmlReportExpect,
+ }}
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ w := runner.NewHTMLResultWriter(tt.buf)
+ err := w.Output(tt.results)
+ assert.NoError(t, err)
+ assert.Equal(t, tt.expect, tt.buf.String())
+ })
+ }
+}
+
+//go:embed testdata/report.html
+var htmlReportExpect string
diff --git a/pkg/runner/writer_markdown.go b/pkg/runner/writer_markdown.go
new file mode 100644
index 0000000..69d24f5
--- /dev/null
+++ b/pkg/runner/writer_markdown.go
@@ -0,0 +1,25 @@
+package runner
+
+import (
+ _ "embed"
+ "io"
+
+ "github.com/linuxsuren/api-testing/pkg/render"
+)
+
+type markdownResultWriter struct {
+ writer io.Writer
+}
+
+// NewMarkdownResultWriter creates the Markdown writer
+func NewMarkdownResultWriter(writer io.Writer) ReportResultWriter {
+ return &markdownResultWriter{writer: writer}
+}
+
+// Output writes the Markdown based report to target writer
+func (w *markdownResultWriter) Output(result []ReportResult) (err error) {
+ return render.RenderThenPrint("md-report", markdownReport, result, w.writer)
+}
+
+//go:embed data/report.md
+var markdownReport string
diff --git a/pkg/runner/writer_markdown_test.go b/pkg/runner/writer_markdown_test.go
new file mode 100644
index 0000000..1a152f9
--- /dev/null
+++ b/pkg/runner/writer_markdown_test.go
@@ -0,0 +1,35 @@
+package runner_test
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/linuxsuren/api-testing/pkg/runner"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMarkdownWriter(t *testing.T) {
+ buf := new(bytes.Buffer)
+ writer := runner.NewMarkdownResultWriter(buf)
+
+ err := writer.Output([]runner.ReportResult{{
+ API: "api",
+ Average: 3,
+ Max: 4,
+ Min: 2,
+ Count: 3,
+ Error: 0,
+ }, {
+ API: "api",
+ Average: 3,
+ Max: 4,
+ Min: 2,
+ Count: 3,
+ Error: 0,
+ }})
+ assert.Nil(t, err)
+ assert.Equal(t, `| API | Average | Max | Min | Count | Error |
+|---|---|---|---|---|---|
+| api | 3ns | 4ns | 2ns | 3 | 0 |
+| api | 3ns | 4ns | 2ns | 3 | 0 |`, buf.String())
+}
diff --git a/pkg/runner/writer_std.go b/pkg/runner/writer_std.go
index da5a052..2a34bd0 100644
--- a/pkg/runner/writer_std.go
+++ b/pkg/runner/writer_std.go
@@ -1,11 +1,9 @@
package runner
import (
- "bytes"
_ "embed"
"fmt"
"io"
- "text/template"
)
type stdResultWriter struct {
@@ -23,36 +21,19 @@ func NewDiscardResultWriter() ReportResultWriter {
}
// Output writer the report to target writer
-func (w *stdResultWriter) Output(result []ReportResult) error {
+func (w *stdResultWriter) Output(results []ReportResult) error {
+ var errResults []ReportResult
fmt.Fprintf(w.writer, "API Average Max Min QPS Count Error\n")
- for _, r := range result {
+ for _, r := range results {
fmt.Fprintf(w.writer, "%s %v %v %v %d %d %d\n", r.API, r.Average, r.Max,
r.Min, r.QPS, r.Count, r.Error)
+ if r.Error > 0 && r.LastErrorMessage != "" {
+ errResults = append(errResults, r)
+ }
+ }
+
+ for _, r := range errResults {
+ fmt.Fprintf(w.writer, "%s error: %s\n", r.API, r.LastErrorMessage)
}
return nil
}
-
-type markdownResultWriter struct {
- writer io.Writer
-}
-
-// NewMarkdownResultWriter creates the Markdown writer
-func NewMarkdownResultWriter(writer io.Writer) ReportResultWriter {
- return &markdownResultWriter{writer: writer}
-}
-
-// Output writer the Markdown based report to target writer
-func (w *markdownResultWriter) Output(result []ReportResult) (err error) {
- var tpl *template.Template
- if tpl, err = template.New("report").Parse(markDownReport); err == nil {
- buf := new(bytes.Buffer)
-
- if err = tpl.Execute(buf, result); err == nil {
- fmt.Fprint(w.writer, buf.String())
- }
- }
- return
-}
-
-//go:embed data/report.md
-var markDownReport string
diff --git a/pkg/runner/writer_std_test.go b/pkg/runner/writer_std_test.go
index 5701b77..f7d2c40 100644
--- a/pkg/runner/writer_std_test.go
+++ b/pkg/runner/writer_std_test.go
@@ -8,33 +8,6 @@ import (
"github.com/stretchr/testify/assert"
)
-func TestMarkdownWriter(t *testing.T) {
- buf := new(bytes.Buffer)
- writer := runner.NewMarkdownResultWriter(buf)
-
- err := writer.Output([]runner.ReportResult{{
- API: "api",
- Average: 3,
- Max: 4,
- Min: 2,
- Count: 3,
- Error: 0,
- }, {
- API: "api",
- Average: 3,
- Max: 4,
- Min: 2,
- Count: 3,
- Error: 0,
- }})
- assert.Nil(t, err)
- assert.Equal(t, `| API | Average | Max | Min | Count | Error |
-|---|---|---|---|---|---|
-| api | 3ns | 4ns | 2ns | 3 | 0 |
-| api | 3ns | 4ns | 2ns | 3 | 0 |
-`, buf.String())
-}
-
func TestNewStdResultWriter(t *testing.T) {
tests := []struct {
name string
@@ -61,6 +34,39 @@ func TestNewStdResultWriter(t *testing.T) {
}},
expect: `API Average Max Min QPS Count Error
api 1ns 1ns 1ns 10 1 0
+`,
+ }, {
+ name: "have errors",
+ buf: new(bytes.Buffer),
+ results: []runner.ReportResult{{
+ API: "api",
+ Average: 1,
+ Max: 1,
+ Min: 1,
+ QPS: 10,
+ Count: 1,
+ Error: 1,
+ LastErrorMessage: "error",
+ }},
+ expect: `API Average Max Min QPS Count Error
+api 1ns 1ns 1ns 10 1 1
+api error: error
+`,
+ }, {
+ name: "have no errors but with message",
+ buf: new(bytes.Buffer),
+ results: []runner.ReportResult{{
+ API: "api",
+ Average: 1,
+ Max: 1,
+ Min: 1,
+ QPS: 10,
+ Count: 1,
+ Error: 0,
+ LastErrorMessage: "message",
+ }},
+ expect: `API Average Max Min QPS Count Error
+api 1ns 1ns 1ns 10 1 0
`,
}}
for _, tt := range tests {
diff --git a/pkg/testing/case.go b/pkg/testing/case.go
index d0138a5..f103762 100644
--- a/pkg/testing/case.go
+++ b/pkg/testing/case.go
@@ -15,7 +15,6 @@ type TestCase struct {
After Job `yaml:"after,omitempty" json:"after"`
Request Request `yaml:"request" json:"request"`
Expect Response `yaml:"expect,omitempty" json:"expect"`
- Clean Clean `yaml:"clean,omitempty" json:"-"`
}
// InScope returns true if the test case is in scope with the given items.
@@ -57,8 +56,3 @@ type Response struct {
Verify []string `yaml:"verify,omitempty" json:"verify,omitempty"`
Schema string `yaml:"schema,omitempty" json:"schema,omitempty"`
}
-
-// Clean represents the clean work after testing
-type Clean struct {
- CleanPrepare bool `yaml:"cleanPrepare"`
-}