feat: adding resource loader interface and default implement (#95)

This commit is contained in:
Rick 2023-06-18 17:43:32 +08:00 committed by GitHub
parent 5a3b1143a3
commit d564f69c9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 231 additions and 89 deletions

View File

@ -5,22 +5,28 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
exec "github.com/linuxsuren/go-fake-runtime" fakeruntime "github.com/linuxsuren/go-fake-runtime"
) )
func TestCreateRunCommand(t *testing.T) { func TestCreateRunCommand(t *testing.T) {
cmd := createRunCommand() cmd := createRunCommand()
assert.Equal(t, "run", cmd.Use) assert.Equal(t, "run", cmd.Use)
init := createInitCommand(exec.FakeExecer{}) init := createInitCommand(fakeruntime.FakeExecer{})
assert.Equal(t, "init", init.Use) assert.Equal(t, "init", init.Use)
server := createServerCmd(&fakeGRPCServer{}) server := createServerCmd(&fakeGRPCServer{})
assert.NotNil(t, server) assert.NotNil(t, server)
assert.Equal(t, "server", server.Use) assert.Equal(t, "server", server.Use)
root := NewRootCmd(exec.FakeExecer{}, NewFakeGRPCServer()) root := NewRootCmd(fakeruntime.FakeExecer{}, NewFakeGRPCServer())
root.SetArgs([]string{"init", "-k=demo.yaml", "--wait-namespace", "demo", "--wait-resource", "demo"}) root.SetArgs([]string{"init", "-k=demo.yaml", "--wait-namespace", "demo", "--wait-resource", "demo"})
err := root.Execute() err := root.Execute()
assert.Nil(t, err) assert.Nil(t, err)
} }
func TestRootCmd(t *testing.T) {
c := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, NewFakeGRPCServer())
assert.NotNil(t, c)
assert.Equal(t, "atest", c.Use)
}

View File

@ -5,8 +5,6 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path"
"path/filepath"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -16,7 +14,6 @@ import (
"github.com/linuxsuren/api-testing/pkg/render" "github.com/linuxsuren/api-testing/pkg/render"
"github.com/linuxsuren/api-testing/pkg/runner" "github.com/linuxsuren/api-testing/pkg/runner"
"github.com/linuxsuren/api-testing/pkg/testing" "github.com/linuxsuren/api-testing/pkg/testing"
"github.com/linuxsuren/api-testing/pkg/util"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/sync/semaphore" "golang.org/x/sync/semaphore"
) )
@ -40,12 +37,16 @@ type runOption struct {
swaggerURL string swaggerURL string
level string level string
caseItems []string caseItems []string
// for internal use
loader testing.Loader
} }
func newDefaultRunOption() *runOption { func newDefaultRunOption() *runOption {
return &runOption{ return &runOption{
reporter: runner.NewMemoryTestReporter(), reporter: runner.NewMemoryTestReporter(),
reportWriter: runner.NewResultWriter(os.Stdout), reportWriter: runner.NewResultWriter(os.Stdout),
loader: testing.NewFileLoader(),
} }
} }
@ -134,19 +135,13 @@ func (o *runOption) runE(cmd *cobra.Command, args []string) (err error) {
o.limiter.Stop() o.limiter.Stop()
}() }()
var suites []string if err = o.loader.Put(o.pattern); err != nil {
for _, pattern := range util.Expand(o.pattern) { return
var files []string
if files, err = filepath.Glob(pattern); err == nil {
suites = append(suites, files...)
}
} }
cmd.Println("found suites:", len(suites)) cmd.Println("found suites:", o.loader.GetCount())
for i := range suites { for o.loader.HasMore() {
item := suites[i] if err = o.runSuiteWithDuration(o.loader); err != nil {
cmd.Println("run suite:", item)
if err = o.runSuiteWithDuration(item); err != nil {
break break
} }
} }
@ -166,7 +161,7 @@ func (o *runOption) runE(cmd *cobra.Command, args []string) (err error) {
return return
} }
func (o *runOption) runSuiteWithDuration(suite string) (err error) { func (o *runOption) runSuiteWithDuration(loader testing.Loader) (err error) {
sem := semaphore.NewWeighted(o.thread) sem := semaphore.NewWeighted(o.thread)
stop := false stop := false
var timeout *time.Ticker var timeout *time.Ticker
@ -204,7 +199,7 @@ func (o *runOption) runSuiteWithDuration(suite string) (err error) {
}() }()
dataContext := getDefaultContext() dataContext := getDefaultContext()
ch <- o.runSuite(suite, dataContext, o.context, stopSingal) ch <- o.runSuite(loader, dataContext, o.context, stopSingal)
}(errChannel, sem) }(errChannel, sem)
if o.duration <= 0 { if o.duration <= 0 {
stop = true stop = true
@ -221,9 +216,14 @@ func (o *runOption) runSuiteWithDuration(suite string) (err error) {
return return
} }
func (o *runOption) runSuite(suite string, dataContext map[string]interface{}, ctx context.Context, stopSingal chan struct{}) (err error) { func (o *runOption) runSuite(loader testing.Loader, dataContext map[string]interface{}, ctx context.Context, stopSingal chan struct{}) (err error) {
var data []byte
if data, err = loader.Load(); err != nil {
return
}
var testSuite *testing.TestSuite var testSuite *testing.TestSuite
if testSuite, err = testing.Parse(suite); err != nil { if testSuite, err = testing.Parse(data); err != nil {
return return
} }
@ -253,7 +253,7 @@ func (o *runOption) runSuite(suite string, dataContext map[string]interface{}, c
o.limiter.Accept() o.limiter.Accept()
ctxWithTimeout, _ := context.WithTimeout(ctx, o.requestTimeout) ctxWithTimeout, _ := context.WithTimeout(ctx, o.requestTimeout)
ctxWithTimeout = context.WithValue(ctxWithTimeout, runner.ContextKey("").ParentDir(), path.Dir(suite)) ctxWithTimeout = context.WithValue(ctxWithTimeout, runner.ContextKey("").ParentDir(), loader.GetContext())
simpleRunner := runner.NewSimpleTestCaseRunner() simpleRunner := runner.NewSimpleTestCaseRunner()
simpleRunner.WithTestReporter(o.reporter) simpleRunner.WithTestReporter(o.reporter)

View File

@ -12,8 +12,8 @@ import (
"github.com/h2non/gock" "github.com/h2non/gock"
"github.com/linuxsuren/api-testing/pkg/limit" "github.com/linuxsuren/api-testing/pkg/limit"
atest "github.com/linuxsuren/api-testing/pkg/testing"
"github.com/linuxsuren/api-testing/pkg/util" "github.com/linuxsuren/api-testing/pkg/util"
fakeruntime "github.com/linuxsuren/go-fake-runtime"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -58,8 +58,13 @@ func TestRunSuite(t *testing.T) {
opt.limiter = limit.NewDefaultRateLimiter(0, 0) opt.limiter = limit.NewDefaultRateLimiter(0, 0)
stopSingal := make(chan struct{}, 1) stopSingal := make(chan struct{}, 1)
err := opt.runSuite(tt.suiteFile, ctx, context.TODO(), stopSingal) loader := atest.NewFileLoader()
assert.Equal(t, tt.hasError, err != nil, err) err := loader.Put(tt.suiteFile)
assert.NoError(t, err)
if loader.HasMore() {
err = opt.runSuite(loader, ctx, context.TODO(), stopSingal)
assert.Equal(t, tt.hasError, err != nil, err)
}
}) })
} }
} }
@ -128,6 +133,11 @@ func TestRunCommand(t *testing.T) {
prepare: fooPrepare, prepare: fooPrepare,
args: []string{"-p", simpleSuite, "--report", "md", "--report-file", path.Join(tmpFile.Name(), "fake")}, args: []string{"-p", simpleSuite, "--report", "md", "--report-file", path.Join(tmpFile.Name(), "fake")},
hasErr: true, hasErr: true,
}, {
name: "malformed report file path",
prepare: fooPrepare,
args: []string{"-p", "[]]$#%*^&()"},
hasErr: true,
}} }}
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -146,12 +156,6 @@ func TestRunCommand(t *testing.T) {
} }
} }
func TestRootCmd(t *testing.T) {
c := NewRootCmd(fakeruntime.FakeExecer{ExpectOS: "linux"}, NewFakeGRPCServer())
assert.NotNil(t, c)
assert.Equal(t, "atest", c.Use)
}
func TestPreRunE(t *testing.T) { func TestPreRunE(t *testing.T) {
tests := []struct { tests := []struct {
name string name string

10
pkg/testing/loader.go Normal file
View File

@ -0,0 +1,10 @@
package testing
// Loader is an interface for test cases loader
type Loader interface {
HasMore() bool
Load() ([]byte, error)
Put(string) (err error)
GetContext() string
GetCount() int
}

View File

@ -0,0 +1,52 @@
package testing
import (
"os"
"path"
"path/filepath"
"github.com/linuxsuren/api-testing/pkg/util"
)
type fileLoader struct {
paths []string
index int
}
// NewFileLoader creates the instance of file loader
func NewFileLoader() Loader {
return &fileLoader{index: -1}
}
// HasMore returns if there are more test cases
func (l *fileLoader) HasMore() bool {
l.index++
return l.index < len(l.paths)
}
// Load returns the test case content
func (l *fileLoader) Load() (data []byte, err error) {
data, err = os.ReadFile(l.paths[l.index])
return
}
// Put adds the test case path
func (l *fileLoader) Put(item string) (err error) {
for _, pattern := range util.Expand(item) {
var files []string
if files, err = filepath.Glob(pattern); err == nil {
l.paths = append(l.paths, files...)
}
}
return
}
// GetContext returns the context of current test case
func (l *fileLoader) GetContext() string {
return path.Dir(l.paths[l.index])
}
// GetCount returns the count of test cases
func (l *fileLoader) GetCount() int {
return len(l.paths)
}

View File

@ -0,0 +1,56 @@
package testing_test
import (
"testing"
atest "github.com/linuxsuren/api-testing/pkg/testing"
"github.com/stretchr/testify/assert"
)
func TestFileLoader(t *testing.T) {
tests := []struct {
name string
items []string
verify func(t *testing.T, loader atest.Loader)
}{{
name: "empty",
items: []string{},
verify: func(t *testing.T, loader atest.Loader) {
assert.False(t, loader.HasMore())
assert.Empty(t, loader.GetCount())
},
}, {
name: "brace expansion path",
items: []string{"testdata/{invalid-,}testcase.yaml"},
verify: defaultVerify,
}, {
name: "glob path",
items: []string{"testdata/*testcase.yaml"},
verify: defaultVerify,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
loader := atest.NewFileLoader()
for _, item := range tt.items {
loader.Put(item)
}
tt.verify(t, loader)
})
}
}
func defaultVerify(t *testing.T, loader atest.Loader) {
assert.True(t, loader.HasMore())
data, err := loader.Load()
assert.Nil(t, err)
assert.Equal(t, invalidTestCaseContent, string(data))
assert.Equal(t, "testdata", loader.GetContext())
assert.True(t, loader.HasMore())
data, err = loader.Load()
assert.Nil(t, err)
assert.Equal(t, testCaseContent, string(data))
assert.Equal(t, "testdata", loader.GetContext())
assert.False(t, loader.HasMore())
}

View File

@ -19,11 +19,8 @@ import (
) )
// Parse parses a file and returns the test suite // Parse parses a file and returns the test suite
func Parse(configFile string) (testSuite *TestSuite, err error) { func Parse(data []byte) (testSuite *TestSuite, err error) {
var data []byte testSuite, err = ParseFromData(data)
if data, err = os.ReadFile(configFile); err == nil {
testSuite, err = ParseFromData(data)
}
// schema validation // schema validation
if err == nil { if err == nil {
@ -116,7 +113,7 @@ func (r *Request) Render(ctx interface{}, dataDir string) (err error) {
} }
// setting default values // setting default values
r.Method = emptyThenDefault(r.Method, http.MethodGet) r.Method = EmptyThenDefault(r.Method, http.MethodGet)
return return
} }
@ -153,18 +150,20 @@ func (r *Request) GetBody() (reader io.Reader, err error) {
// Render renders the response // Render renders the response
func (r *Response) Render(ctx interface{}) (err error) { func (r *Response) Render(ctx interface{}) (err error) {
r.StatusCode = zeroThenDefault(r.StatusCode, http.StatusOK) r.StatusCode = ZeroThenDefault(r.StatusCode, http.StatusOK)
return return
} }
func zeroThenDefault(val, defVal int) int { // ZeroThenDefault return the default value if the val is zero
func ZeroThenDefault(val, defVal int) int {
if val == 0 { if val == 0 {
val = defVal val = defVal
} }
return val return val
} }
func emptyThenDefault(val, defVal string) string { // EmptyThenDefault return the default value if the val is empty
func EmptyThenDefault(val, defVal string) string {
if strings.TrimSpace(val) == "" { if strings.TrimSpace(val) == "" {
val = defVal val = defVal
} }

View File

@ -1,150 +1,162 @@
package testing package testing_test
import ( import (
"io" "io"
"net/http" "net/http"
"os"
"testing" "testing"
_ "embed" _ "embed"
atest "github.com/linuxsuren/api-testing/pkg/testing"
"github.com/linuxsuren/api-testing/pkg/util" "github.com/linuxsuren/api-testing/pkg/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestParse(t *testing.T) { func TestParse(t *testing.T) {
suite, err := Parse("../../sample/testsuite-gitlab.yaml") data, err := os.ReadFile("../../sample/testsuite-gitlab.yaml")
if !assert.NoError(t, err) {
return
}
suite, err := atest.Parse(data)
if assert.Nil(t, err) && assert.NotNil(t, suite) { if assert.Nil(t, err) && assert.NotNil(t, suite) {
assert.Equal(t, "Gitlab", suite.Name) assert.Equal(t, "Gitlab", suite.Name)
assert.Equal(t, 2, len(suite.Items)) assert.Equal(t, 2, len(suite.Items))
assert.Equal(t, TestCase{ assert.Equal(t, atest.TestCase{
Name: "projects", Name: "projects",
Request: Request{ Request: atest.Request{
API: "https://gitlab.com/api/v4/projects", API: "https://gitlab.com/api/v4/projects",
}, },
Expect: Response{ Expect: atest.Response{
StatusCode: http.StatusOK, StatusCode: http.StatusOK,
Schema: `{ Schema: `{
"type": "array" "type": "array"
} }
`, `,
}, },
Before: Job{ Before: atest.Job{
Items: []string{"sleep(1)"}, Items: []string{"sleep(1)"},
}, },
After: Job{ After: atest.Job{
Items: []string{"sleep(1)"}, Items: []string{"sleep(1)"},
}, },
}, suite.Items[0]) }, suite.Items[0])
} }
_, err = Parse("testdata/invalid-testcase.yaml") _, err = atest.Parse([]byte(invalidTestCaseContent))
assert.NotNil(t, err) assert.NotNil(t, err)
} }
func TestDuplicatedNames(t *testing.T) { func TestDuplicatedNames(t *testing.T) {
_, err := Parse("testdata/duplicated-names.yaml") data, err := os.ReadFile("testdata/duplicated-names.yaml")
if !assert.NoError(t, err) {
return
}
_, err = atest.Parse(data)
assert.NotNil(t, err) assert.NotNil(t, err)
_, err = ParseFromData([]byte("fake")) _, err = atest.ParseFromData([]byte("fake"))
assert.NotNil(t, err) assert.NotNil(t, err)
} }
func TestRequestRender(t *testing.T) { func TestRequestRender(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
request *Request request *atest.Request
verify func(t *testing.T, req *Request) verify func(t *testing.T, req *atest.Request)
ctx interface{} ctx interface{}
hasErr bool hasErr bool
}{{ }{{
name: "slice as context", name: "slice as context",
request: &Request{ request: &atest.Request{
API: "http://localhost/{{index . 0}}", API: "http://localhost/{{index . 0}}",
Body: "{{index . 1}}", Body: "{{index . 1}}",
}, },
ctx: []string{"foo", "bar"}, ctx: []string{"foo", "bar"},
hasErr: false, hasErr: false,
verify: func(t *testing.T, req *Request) { verify: func(t *testing.T, req *atest.Request) {
assert.Equal(t, "http://localhost/foo", req.API) assert.Equal(t, "http://localhost/foo", req.API)
assert.Equal(t, "bar", req.Body) assert.Equal(t, "bar", req.Body)
}, },
}, { }, {
name: "default values", name: "default values",
request: &Request{}, request: &atest.Request{},
verify: func(t *testing.T, req *Request) { verify: func(t *testing.T, req *atest.Request) {
assert.Equal(t, http.MethodGet, req.Method) assert.Equal(t, http.MethodGet, req.Method)
}, },
hasErr: false, hasErr: false,
}, { }, {
name: "context is nil", name: "context is nil",
request: &Request{}, request: &atest.Request{},
ctx: nil, ctx: nil,
hasErr: false, hasErr: false,
}, { }, {
name: "body from file", name: "body from file",
request: &Request{ request: &atest.Request{
BodyFromFile: "testdata/generic_body.json", BodyFromFile: "testdata/generic_body.json",
}, },
ctx: TestCase{ ctx: atest.TestCase{
Name: "linuxsuren", Name: "linuxsuren",
}, },
hasErr: false, hasErr: false,
verify: func(t *testing.T, req *Request) { verify: func(t *testing.T, req *atest.Request) {
assert.Equal(t, `{"name": "linuxsuren"}`, req.Body) assert.Equal(t, `{"name": "linuxsuren"}`, req.Body)
}, },
}, { }, {
name: "body file not found", name: "body file not found",
request: &Request{ request: &atest.Request{
BodyFromFile: "testdata/fake", BodyFromFile: "testdata/fake",
}, },
hasErr: true, hasErr: true,
}, { }, {
name: "invalid API as template", name: "invalid API as template",
request: &Request{ request: &atest.Request{
API: "{{.name}", API: "{{.name}",
}, },
hasErr: true, hasErr: true,
}, { }, {
name: "failed with API render", name: "failed with API render",
request: &Request{ request: &atest.Request{
API: "{{.name}}", API: "{{.name}}",
}, },
ctx: TestCase{}, ctx: atest.TestCase{},
hasErr: true, hasErr: true,
}, { }, {
name: "invalid body as template", name: "invalid body as template",
request: &Request{ request: &atest.Request{
Body: "{{.name}", Body: "{{.name}",
}, },
hasErr: true, hasErr: true,
}, { }, {
name: "failed with body render", name: "failed with body render",
request: &Request{ request: &atest.Request{
Body: "{{.name}}", Body: "{{.name}}",
}, },
ctx: TestCase{}, ctx: atest.TestCase{},
hasErr: true, hasErr: true,
}, { }, {
name: "form render", name: "form render",
request: &Request{ request: &atest.Request{
Form: map[string]string{ Form: map[string]string{
"key": "{{.Name}}", "key": "{{.Name}}",
}, },
}, },
ctx: TestCase{Name: "linuxsuren"}, ctx: atest.TestCase{Name: "linuxsuren"},
verify: func(t *testing.T, req *Request) { verify: func(t *testing.T, req *atest.Request) {
assert.Equal(t, "linuxsuren", req.Form["key"]) assert.Equal(t, "linuxsuren", req.Form["key"])
}, },
hasErr: false, hasErr: false,
}, { }, {
name: "header render", name: "header render",
request: &Request{ request: &atest.Request{
Header: map[string]string{ Header: map[string]string{
"key": "{{.Name}}", "key": "{{.Name}}",
}, },
}, },
ctx: TestCase{Name: "linuxsuren"}, ctx: atest.TestCase{Name: "linuxsuren"},
verify: func(t *testing.T, req *Request) { verify: func(t *testing.T, req *atest.Request) {
assert.Equal(t, "linuxsuren", req.Header["key"]) assert.Equal(t, "linuxsuren", req.Header["key"])
}, },
hasErr: false, hasErr: false,
@ -162,14 +174,14 @@ func TestRequestRender(t *testing.T) {
func TestResponseRender(t *testing.T) { func TestResponseRender(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
response *Response response *atest.Response
verify func(t *testing.T, req *Response) verify func(t *testing.T, req *atest.Response)
ctx interface{} ctx interface{}
hasErr bool hasErr bool
}{{ }{{
name: "blank response", name: "blank response",
response: &Response{}, response: &atest.Response{},
verify: func(t *testing.T, req *Response) { verify: func(t *testing.T, req *atest.Response) {
assert.Equal(t, http.StatusOK, req.StatusCode) assert.Equal(t, http.StatusOK, req.StatusCode)
}, },
hasErr: false, hasErr: false,
@ -208,24 +220,24 @@ func TestEmptyThenDefault(t *testing.T) {
}} }}
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result := emptyThenDefault(tt.val, tt.defVal) result := atest.EmptyThenDefault(tt.val, tt.defVal)
assert.Equal(t, tt.expect, result, result) assert.Equal(t, tt.expect, result, result)
}) })
} }
assert.Equal(t, 1, zeroThenDefault(0, 1)) assert.Equal(t, 1, atest.ZeroThenDefault(0, 1))
assert.Equal(t, 1, zeroThenDefault(1, 2)) assert.Equal(t, 1, atest.ZeroThenDefault(1, 2))
} }
func TestTestCase(t *testing.T) { func TestTestCase(t *testing.T) {
testCase, err := ParseTestCaseFromData([]byte(testCaseContent)) testCase, err := atest.ParseTestCaseFromData([]byte(testCaseContent))
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, &TestCase{ assert.Equal(t, &atest.TestCase{
Name: "projects", Name: "projects",
Request: Request{ Request: atest.Request{
API: "https://foo", API: "https://foo",
}, },
Expect: Response{ Expect: atest.Response{
StatusCode: http.StatusOK, StatusCode: http.StatusOK,
}, },
}, testCase) }, testCase)
@ -236,21 +248,21 @@ func TestGetBody(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
req *Request req *atest.Request
expectBody string expectBody string
containBody string containBody string
expectErr bool expectErr bool
}{{ }{{
name: "normal body", name: "normal body",
req: &Request{Body: defaultBody}, req: &atest.Request{Body: defaultBody},
expectBody: defaultBody, expectBody: defaultBody,
}, { }, {
name: "body from file", name: "body from file",
req: &Request{BodyFromFile: "testdata/testcase.yaml"}, req: &atest.Request{BodyFromFile: "testdata/testcase.yaml"},
expectBody: testCaseContent, expectBody: testCaseContent,
}, { }, {
name: "multipart form data", name: "multipart form data",
req: &Request{ req: &atest.Request{
Header: map[string]string{ Header: map[string]string{
util.ContentType: util.MultiPartFormData, util.ContentType: util.MultiPartFormData,
}, },
@ -261,7 +273,7 @@ func TestGetBody(t *testing.T) {
containBody: "name=\"key\"\r\n\r\nvalue\r\n", containBody: "name=\"key\"\r\n\r\nvalue\r\n",
}, { }, {
name: "normal form", name: "normal form",
req: &Request{ req: &atest.Request{
Header: map[string]string{ Header: map[string]string{
util.ContentType: util.Form, util.ContentType: util.Form,
}, },
@ -292,3 +304,6 @@ func TestGetBody(t *testing.T) {
//go:embed testdata/testcase.yaml //go:embed testdata/testcase.yaml
var testCaseContent string var testCaseContent string
//go:embed testdata/invalid-testcase.yaml
var invalidTestCaseContent string