Skip to content

feat: add API and expect status code default value #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
52 changes: 37 additions & 15 deletions cmd/run.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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
Expand All @@ -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)

Expand Down
97 changes: 97 additions & 0 deletions cmd/run_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
5 changes: 5 additions & 0 deletions cmd/testdata/simple-suite.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name: Simple
api: http://foo/
items:
- request:
api: /bar
11 changes: 6 additions & 5 deletions pkg/runner/simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions pkg/testing/case.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

Expand Down
24 changes: 24 additions & 0 deletions pkg/testing/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package testing
import (
"bytes"
"html/template"
"net/http"
"os"
"strings"

Expand Down Expand Up @@ -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
}
67 changes: 66 additions & 1 deletion pkg/testing/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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{},
Expand Down Expand Up @@ -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))
}