diff --git a/README.md b/README.md index 724698b8..068a89d8 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,19 @@ This is a API testing tool. ## Feature -* Response Body fields equation check -* Response Body [eval](https://expr.medv.io/) -* Output reference between TestCase +* Response Body fields equation check +* Response Body [eval](https://expr.medv.io/) +* Output reference between TestCase ## Template The following fields are templated with [sprig](http://masterminds.github.io/sprig/): -* API -* Request Body +* API +* Request Body + +## TODO +* Reduce the size of context +* Support customized context ## Limit -* Only support to parse the response body when it's a map +* Only support to parse the response body when it's a map or array diff --git a/cmd/run.go b/cmd/run.go index fe2eae64..f455164f 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -1,8 +1,10 @@ package cmd import ( + "fmt" "path" "path/filepath" + "strings" "github.com/linuxsuren/api-testing/pkg/runner" "github.com/linuxsuren/api-testing/pkg/testing" @@ -17,8 +19,12 @@ type runOption struct { func CreateRunCommand() (cmd *cobra.Command) { opt := &runOption{} cmd = &cobra.Command{ - Use: "run", - RunE: opt.runE, + Use: "run", + Aliases: []string{"r"}, + Example: `atest run -p sample.yaml +See also https://github.com/LinuxSuRen/api-testing/tree/master/sample`, + Short: "Run the test suite", + RunE: opt.runE, } // set flags @@ -30,31 +36,47 @@ func CreateRunCommand() (cmd *cobra.Command) { func (o *runOption) runE(cmd *cobra.Command, args []string) (err error) { var files []string - - ctx := map[string]interface{}{} + ctx := getDefaultContext() if files, err = filepath.Glob(o.pattern); err == nil { for i := range files { item := files[i] - - var testSuite *testing.TestSuite - if testSuite, err = testing.Parse(item); err != nil { + fmt.Println(item, "===", o.pattern, args) + if err = runSuite(item, ctx); err != nil { return } + } + } + return +} - for _, testCase := range testSuite.Items { - setRelativeDir(item, &testCase) - var output interface{} - if output, err = runner.RunTestCase(&testCase, ctx); err != nil { - return - } - ctx[testCase.Name] = output - } +func runSuite(suite string, ctx map[string]interface{}) (err error) { + var testSuite *testing.TestSuite + if testSuite, err = testing.Parse(suite); err != nil { + return + } + + testSuite.API = strings.TrimSuffix(testSuite.API, "/") + for _, testCase := range testSuite.Items { + // reuse the API prefix + if strings.HasPrefix(testCase.Request.API, "/") { + testCase.Request.API = fmt.Sprintf("%s/%s", testSuite.API, testCase.Request.API) + } + + setRelativeDir(suite, &testCase) + var output interface{} + if output, err = runner.RunTestCase(&testCase, ctx); err != nil { + return } + ctx[testCase.Name] = output } return } +func getDefaultContext() map[string]interface{} { + return map[string]interface{}{} +} + func setRelativeDir(configFile string, testcase *testing.TestCase) { dir := filepath.Dir(configFile) diff --git a/cmd/run_test.go b/cmd/run_test.go new file mode 100644 index 00000000..09791b0e --- /dev/null +++ b/cmd/run_test.go @@ -0,0 +1,97 @@ +package cmd + +import ( + "fmt" + "net/http" + "testing" + + "github.com/h2non/gock" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestRunSuite(t *testing.T) { + tests := []struct { + name string + suiteFile string + prepare func() + hasError bool + }{{ + name: "simple", + suiteFile: "testdata/simple-suite.yaml", + prepare: func() { + gock.New("http://foo"). + Get("/bar"). + Reply(http.StatusOK). + JSON("{}") + }, + hasError: false, + }, { + name: "response is not JSON", + suiteFile: "testdata/simple-suite.yaml", + prepare: func() { + gock.New("http://foo"). + Get("/bar"). + Reply(http.StatusOK) + }, + hasError: true, + }, { + name: "not found file", + suiteFile: "testdata/fake.yaml", + prepare: func() {}, + hasError: true, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer gock.Clean() + + tt.prepare() + ctx := getDefaultContext() + err := runSuite(tt.suiteFile, ctx) + assert.Equal(t, tt.hasError, err != nil, err) + }) + } +} + +func TestRunCommand(t *testing.T) { + tests := []struct { + name string + args []string + prepare func() + hasErr bool + }{{ + name: "status code is not match", + args: []string{"-p", "testdata/simple-suite.yaml"}, + prepare: func() { + gock.New("http://foo").Get("/bar") + }, + hasErr: true, + }, { + name: "file not found", + args: []string{"--pattern", "fake"}, + prepare: func() {}, + hasErr: false, + }, { + name: "normal case", + args: []string{"-p", "testdata/simple-suite.yaml"}, + prepare: func() { + gock.New("http://foo").Get("/bar").Reply(http.StatusOK).JSON("{}") + }, + hasErr: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer gock.Clean() + tt.prepare() + + root := &cobra.Command{Use: "root"} + root.AddCommand(CreateRunCommand()) + + root.SetArgs(append([]string{"run"}, tt.args...)) + + fmt.Println(tt.args) + err := root.Execute() + assert.Equal(t, tt.hasErr, err != nil, err) + }) + } +} diff --git a/cmd/testdata/simple-suite.yaml b/cmd/testdata/simple-suite.yaml new file mode 100644 index 00000000..53e21551 --- /dev/null +++ b/cmd/testdata/simple-suite.yaml @@ -0,0 +1,5 @@ +name: Simple +api: http://foo/ +items: +- request: + api: /bar diff --git a/pkg/runner/simple.go b/pkg/runner/simple.go index cfb7b4b6..a0bff440 100644 --- a/pkg/runner/simple.go +++ b/pkg/runner/simple.go @@ -95,11 +95,12 @@ func RunTestCase(testcase *testing.TestCase, ctx interface{}) (output interface{ return } - if testcase.Expect.StatusCode != 0 { - if err = expectInt(testcase.Name, testcase.Expect.StatusCode, resp.StatusCode); err != nil { - err = fmt.Errorf("error is: %v\n%s", err, string(responseBodyData)) - return - } + if err = testcase.Expect.Render(nil); err != nil { + return + } + if err = expectInt(testcase.Name, testcase.Expect.StatusCode, resp.StatusCode); err != nil { + err = fmt.Errorf("error is: %v\n%s", err, string(responseBodyData)) + return } for key, val := range testcase.Expect.Header { diff --git a/pkg/testing/case.go b/pkg/testing/case.go index 869d48da..c4311db2 100644 --- a/pkg/testing/case.go +++ b/pkg/testing/case.go @@ -3,6 +3,7 @@ package testing // TestSuite represents a set of test cases type TestSuite struct { Name string `yaml:"name"` + API string `yaml:"api"` Items []TestCase `yaml:"items"` } diff --git a/pkg/testing/parser.go b/pkg/testing/parser.go index 055f8b4d..a5a13ca6 100644 --- a/pkg/testing/parser.go +++ b/pkg/testing/parser.go @@ -3,6 +3,7 @@ package testing import ( "bytes" "html/template" + "net/http" "os" "strings" @@ -69,5 +70,28 @@ func (r *Request) Render(ctx interface{}) (err error) { } } } + + // setting default values + r.Method = emptyThenDefault(r.Method, http.MethodGet) return } + +// Render renders the response +func (r *Response) Render(ctx interface{}) (err error) { + r.StatusCode = zeroThenDefault(r.StatusCode, http.StatusOK) + return +} + +func zeroThenDefault(val, defVal int) int { + if val == 0 { + val = defVal + } + return val +} + +func emptyThenDefault(val, defVal string) string { + if strings.TrimSpace(val) == "" { + val = defVal + } + return val +} diff --git a/pkg/testing/parser_test.go b/pkg/testing/parser_test.go index 51ceb6fe..d80b8b2f 100644 --- a/pkg/testing/parser_test.go +++ b/pkg/testing/parser_test.go @@ -24,7 +24,7 @@ func TestParse(t *testing.T) { } } -func TestRender(t *testing.T) { +func TestRequestRender(t *testing.T) { tests := []struct { name string request *Request @@ -43,6 +43,13 @@ func TestRender(t *testing.T) { assert.Equal(t, "http://localhost/foo", req.API) assert.Equal(t, "bar", req.Body) }, + }, { + name: "default values", + request: &Request{}, + verify: func(t *testing.T, req *Request) { + assert.Equal(t, http.MethodGet, req.Method) + }, + hasErr: false, }, { name: "context is nil", request: &Request{}, @@ -126,3 +133,61 @@ func TestRender(t *testing.T) { }) } } + +func TestResponseRender(t *testing.T) { + tests := []struct { + name string + response *Response + verify func(t *testing.T, req *Response) + ctx interface{} + hasErr bool + }{{ + name: "blank response", + response: &Response{}, + verify: func(t *testing.T, req *Response) { + assert.Equal(t, http.StatusOK, req.StatusCode) + }, + hasErr: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.response.Render(tt.ctx) + if assert.Equal(t, tt.hasErr, err != nil, err) && tt.verify != nil { + tt.verify(t, tt.response) + } + }) + } +} + +func TestEmptyThenDefault(t *testing.T) { + tests := []struct { + name string + val string + defVal string + expect string + }{{ + name: "empty string", + val: "", + defVal: "abc", + expect: "abc", + }, { + name: "blank string", + val: " ", + defVal: "abc", + expect: "abc", + }, { + name: "not empty or blank string", + val: "abc", + defVal: "def", + expect: "abc", + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := emptyThenDefault(tt.val, tt.defVal) + assert.Equal(t, tt.expect, result, result) + }) + } + + assert.Equal(t, 1, zeroThenDefault(0, 1)) + assert.Equal(t, 1, zeroThenDefault(1, 2)) +}