diff --git a/README.md b/README.md index 2aaf1bd9..ef3161e6 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,13 @@ This is a API testing tool. -## Feature +## Features * Response Body fields equation check * Response Body [eval](https://expr.medv.io/) * Verify the Kubernetes resources * Validate the response body with [JSON schema](https://json-schema.org/) +* Pre and post handle with the API request * Output reference between TestCase * Run in server mode, and provide the gRPC endpoint * [VS Code extension](https://github.com/LinuxSuRen/vscode-api-testing) support @@ -71,6 +72,7 @@ You could use all the common functions which comes from [sprig](http://mastermin | Name | Usage | |---|---| | `randomKubernetesName` | `{{randomKubernetesName}}` to generate Kubernetes resource name randomly, the name will have 8 chars | +| `sleep` | `{{sleep(1)}}` in the pre and post request handle | ## Verify against Kubernetes diff --git a/cmd/root_test.go b/cmd/root_test.go index e7a704e8..3047559e 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -5,41 +5,9 @@ import ( "github.com/stretchr/testify/assert" - atesting "github.com/linuxsuren/api-testing/pkg/testing" exec "github.com/linuxsuren/go-fake-runtime" ) -func TestSetRelativeDir(t *testing.T) { - type args struct { - configFile string - testcase *atesting.TestCase - } - tests := []struct { - name string - args args - verify func(*testing.T, *atesting.TestCase) - }{{ - name: "normal", - args: args{ - configFile: "a/b/c.yaml", - testcase: &atesting.TestCase{ - Prepare: atesting.Prepare{ - Kubernetes: []string{"deploy.yaml"}, - }, - }, - }, - verify: func(t *testing.T, testCase *atesting.TestCase) { - assert.Equal(t, "a/b/deploy.yaml", testCase.Prepare.Kubernetes[0]) - }, - }} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - setRelativeDir(tt.args.configFile, tt.args.testcase) - tt.verify(t, tt.args.testcase) - }) - } -} - func TestCreateRunCommand(t *testing.T) { cmd := createRunCommand() assert.Equal(t, "run", cmd.Use) diff --git a/cmd/run.go b/cmd/run.go index bcf21b8f..292f24ea 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "os" - "path" "path/filepath" "strings" "sync" @@ -216,7 +215,6 @@ func (o *runOption) runSuite(suite string, dataContext map[string]interface{}, c case <-stopSingal: return default: - setRelativeDir(suite, &testCase) o.limiter.Accept() ctxWithTimeout, _ := context.WithTimeout(ctx, o.requestTimeout) @@ -238,11 +236,3 @@ func (o *runOption) runSuite(suite string, dataContext map[string]interface{}, c func getDefaultContext() map[string]interface{} { return map[string]interface{}{} } - -func setRelativeDir(configFile string, testcase *testing.TestCase) { - dir := filepath.Dir(configFile) - - for i := range testcase.Prepare.Kubernetes { - testcase.Prepare.Kubernetes[i] = path.Join(dir, testcase.Prepare.Kubernetes[i]) - } -} diff --git a/pkg/runner/expr_function.go b/pkg/runner/expr_function.go new file mode 100644 index 00000000..5adfdf8f --- /dev/null +++ b/pkg/runner/expr_function.go @@ -0,0 +1,26 @@ +// Package runner provides the common expr style functions +package runner + +import ( + "fmt" + "time" +) + +// ExprFuncSleep is a expr function for sleeping +func ExprFuncSleep(params ...interface{}) (res interface{}, err error) { + if len(params) < 1 { + err = fmt.Errorf("the duration param is required") + return + } + + switch duration := params[0].(type) { + case int: + time.Sleep(time.Duration(duration) * time.Second) + case string: + var dur time.Duration + if dur, err = time.ParseDuration(duration); err == nil { + time.Sleep(dur) + } + } + return +} diff --git a/pkg/runner/expr_function_test.go b/pkg/runner/expr_function_test.go new file mode 100644 index 00000000..e2f3be5f --- /dev/null +++ b/pkg/runner/expr_function_test.go @@ -0,0 +1,33 @@ +package runner_test + +import ( + "testing" + + "github.com/linuxsuren/api-testing/pkg/runner" + "github.com/stretchr/testify/assert" +) + +func TestExprFuncSleep(t *testing.T) { + tests := []struct { + name string + params []interface{} + hasErr bool + }{{ + name: "string format duration", + params: []interface{}{"0.01s"}, + hasErr: false, + }, { + name: "without params", + hasErr: true, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := runner.ExprFuncSleep(tt.params...) + if tt.hasErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/runner/simple.go b/pkg/runner/simple.go index 6b76e45a..9461200b 100644 --- a/pkg/runner/simple.go +++ b/pkg/runner/simple.go @@ -180,15 +180,14 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte r.testReporter.PutRecord(rr) }(record) - if err = r.doPrepare(testcase); err != nil { - err = fmt.Errorf("failed to prepare, error: %v", err) - return - } - defer func() { if testcase.Clean.CleanPrepare { err = r.doCleanPrepare(testcase) } + + if err == nil { + err = runJob(testcase.After) + } }() client := http.Client{ @@ -216,6 +215,10 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte request.Header.Add(key, val) } + if err = runJob(testcase.Before); err != nil { + return + } + r.log.Info("start to send request to %s\n", testcase.Request.API) // TODO only do this for unit testing, should remove it once we have a better way @@ -285,21 +288,10 @@ func (r *simpleTestCaseRunner) WithExecer(execer fakeruntime.Execer) TestCaseRun return r } -func (r *simpleTestCaseRunner) doPrepare(testcase *testing.TestCase) (err error) { - for i := range testcase.Prepare.Kubernetes { - item := testcase.Prepare.Kubernetes[i] - - if err = r.execer.RunCommand("kubectl", "apply", "-f", item); err != nil { - return - } - } - return -} - func (r *simpleTestCaseRunner) doCleanPrepare(testcase *testing.TestCase) (err error) { - count := len(testcase.Prepare.Kubernetes) + count := len(testcase.Before.Items) for i := count - 1; i >= 0; i-- { - item := testcase.Prepare.Kubernetes[i] + item := testcase.Before.Items[i] if err = r.execer.RunCommand("kubectl", "delete", "-f", item); err != nil { return @@ -413,3 +405,22 @@ func verifyResponseBodyData(caseName string, expect testing.Response, responseBo } return } + +func runJob(job testing.Job) (err error) { + var program *vm.Program + env := struct{}{} + + for _, item := range job.Items { + if program, err = expr.Compile(item, expr.Env(env), + expr.Function("sleep", ExprFuncSleep)); err != nil { + fmt.Printf("failed to compile: %s, %v\n", item, err) + return + } + + if _, err = expr.Run(program, env); err != nil { + fmt.Printf("failed to Run: %s, %v\n", item, err) + return + } + } + return +} diff --git a/pkg/runner/simple_test.go b/pkg/runner/simple_test.go index 049952cc..1e217297 100644 --- a/pkg/runner/simple_test.go +++ b/pkg/runner/simple_test.go @@ -43,8 +43,8 @@ func TestTestCase(t *testing.T) { }{{ name: "failed during the prepare stage", testCase: &atest.TestCase{ - Prepare: atest.Prepare{ - Kubernetes: []string{"demo.yaml"}, + Before: atest.Job{ + Items: []string{"demo.yaml"}, }, }, execer: fakeruntime.FakeExecer{ExpectError: errors.New("fake")}, @@ -69,8 +69,8 @@ func TestTestCase(t *testing.T) { `data.name == "linuxsuren"`, }, }, - Prepare: atest.Prepare{ - Kubernetes: []string{"demo.yaml"}, + Before: atest.Job{ + Items: []string{"sleep(1)"}, }, Clean: atest.Clean{ CleanPrepare: true, @@ -411,6 +411,32 @@ func TestJSONSchemaValidation(t *testing.T) { } } +func TestRunJob(t *testing.T) { + tests := []struct { + name string + job atest.Job + hasErr bool + }{{ + name: "sleep 1s", + job: atest.Job{ + Items: []string{"sleep(1)"}, + }, + hasErr: false, + }, { + name: "no params", + job: atest.Job{ + Items: []string{"sleep()"}, + }, + hasErr: true, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := runJob(tt.job) + assert.Equal(t, tt.hasErr, err != nil, err) + }) + } +} + const defaultSchemaForTest = `{"properties": { "name": {"type": "string"}, "age": {"type": "integer"} diff --git a/pkg/testing/case.go b/pkg/testing/case.go index a6f13f34..3af140ef 100644 --- a/pkg/testing/case.go +++ b/pkg/testing/case.go @@ -11,7 +11,8 @@ type TestSuite struct { type TestCase struct { Name string `yaml:"name" json:"name"` Group string - Prepare Prepare `yaml:"prepare" json:"-"` + Before Job `yaml:"before" json:"before"` + After Job `yaml:"after" json:"after"` Request Request `yaml:"request" json:"request"` Expect Response `yaml:"expect" json:"expect"` Clean Clean `yaml:"clean" json:"-"` @@ -31,9 +32,9 @@ func (c *TestCase) InScope(items []string) bool { return false } -// Prepare does the prepare work -type Prepare struct { - Kubernetes []string `yaml:"kubernetes"` +// Job contains a list of jobs +type Job struct { + Items []string `yaml:"items"` } // Request represents a HTTP request diff --git a/pkg/testing/parser_test.go b/pkg/testing/parser_test.go index 48f8c14f..b506f4e7 100644 --- a/pkg/testing/parser_test.go +++ b/pkg/testing/parser_test.go @@ -28,6 +28,12 @@ func TestParse(t *testing.T) { } `, }, + Before: Job{ + Items: []string{"sleep(1)"}, + }, + After: Job{ + Items: []string{"sleep(1)"}, + }, }, suite.Items[0]) } diff --git a/sample/api-testing-schema.json b/sample/api-testing-schema.json index 9af0f30f..81fcdba4 100644 --- a/sample/api-testing-schema.json +++ b/sample/api-testing-schema.json @@ -37,6 +37,12 @@ }, "expect": { "$ref": "#/definitions/Expect" + }, + "before": { + "$ref": "#/definitions/Job" + }, + "after": { + "$ref": "#/definitions/Job" } }, "required": [ @@ -121,6 +127,22 @@ "api" ], "title": "Request" + }, + "Job": { + "type": "object", + "additionalProperties": false, + "properties": { + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "items" + ], + "title": "Job" } } } diff --git a/sample/testsuite-gitee.yaml b/sample/testsuite-gitee.yaml index d1d2f733..1043d892 100644 --- a/sample/testsuite-gitee.yaml +++ b/sample/testsuite-gitee.yaml @@ -4,6 +4,9 @@ name: Gitee api: https://gitee.com/api/v5 items: - name: stargazers + before: + items: + - sleep(1) request: api: /repos/linuxsuren/api-testing/stargazers expect: diff --git a/sample/testsuite-gitlab.yaml b/sample/testsuite-gitlab.yaml index df169605..346aa874 100644 --- a/sample/testsuite-gitlab.yaml +++ b/sample/testsuite-gitlab.yaml @@ -12,6 +12,12 @@ items: { "type": "array" } + before: + items: + - "sleep(1)" + after: + items: + - "sleep(1)" - name: project request: api: https://gitlab.com/api/v4/projects/{{int64 (index .projects 0).id}}