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"
exec "github.com/linuxsuren/go-fake-runtime"
fakeruntime "github.com/linuxsuren/go-fake-runtime"
)
func TestCreateRunCommand(t *testing.T) {
cmd := createRunCommand()
assert.Equal(t, "run", cmd.Use)
init := createInitCommand(exec.FakeExecer{})
init := createInitCommand(fakeruntime.FakeExecer{})
assert.Equal(t, "init", init.Use)
server := createServerCmd(&fakeGRPCServer{})
assert.NotNil(t, server)
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"})
err := root.Execute()
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"
"io"
"os"
"path"
"path/filepath"
"strings"
"sync"
"time"
@ -16,7 +14,6 @@ import (
"github.com/linuxsuren/api-testing/pkg/render"
"github.com/linuxsuren/api-testing/pkg/runner"
"github.com/linuxsuren/api-testing/pkg/testing"
"github.com/linuxsuren/api-testing/pkg/util"
"github.com/spf13/cobra"
"golang.org/x/sync/semaphore"
)
@ -40,12 +37,16 @@ type runOption struct {
swaggerURL string
level string
caseItems []string
// for internal use
loader testing.Loader
}
func newDefaultRunOption() *runOption {
return &runOption{
reporter: runner.NewMemoryTestReporter(),
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()
}()
var suites []string
for _, pattern := range util.Expand(o.pattern) {
var files []string
if files, err = filepath.Glob(pattern); err == nil {
suites = append(suites, files...)
}
if err = o.loader.Put(o.pattern); err != nil {
return
}
cmd.Println("found suites:", len(suites))
for i := range suites {
item := suites[i]
cmd.Println("run suite:", item)
if err = o.runSuiteWithDuration(item); err != nil {
cmd.Println("found suites:", o.loader.GetCount())
for o.loader.HasMore() {
if err = o.runSuiteWithDuration(o.loader); err != nil {
break
}
}
@ -166,7 +161,7 @@ func (o *runOption) runE(cmd *cobra.Command, args []string) (err error) {
return
}
func (o *runOption) runSuiteWithDuration(suite string) (err error) {
func (o *runOption) runSuiteWithDuration(loader testing.Loader) (err error) {
sem := semaphore.NewWeighted(o.thread)
stop := false
var timeout *time.Ticker
@ -204,7 +199,7 @@ func (o *runOption) runSuiteWithDuration(suite string) (err error) {
}()
dataContext := getDefaultContext()
ch <- o.runSuite(suite, dataContext, o.context, stopSingal)
ch <- o.runSuite(loader, dataContext, o.context, stopSingal)
}(errChannel, sem)
if o.duration <= 0 {
stop = true
@ -221,9 +216,14 @@ func (o *runOption) runSuiteWithDuration(suite string) (err error) {
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
if testSuite, err = testing.Parse(suite); err != nil {
if testSuite, err = testing.Parse(data); err != nil {
return
}
@ -253,7 +253,7 @@ func (o *runOption) runSuite(suite string, dataContext map[string]interface{}, c
o.limiter.Accept()
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.WithTestReporter(o.reporter)

View File

@ -12,8 +12,8 @@ import (
"github.com/h2non/gock"
"github.com/linuxsuren/api-testing/pkg/limit"
atest "github.com/linuxsuren/api-testing/pkg/testing"
"github.com/linuxsuren/api-testing/pkg/util"
fakeruntime "github.com/linuxsuren/go-fake-runtime"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)
@ -58,8 +58,13 @@ func TestRunSuite(t *testing.T) {
opt.limiter = limit.NewDefaultRateLimiter(0, 0)
stopSingal := make(chan struct{}, 1)
err := opt.runSuite(tt.suiteFile, ctx, context.TODO(), stopSingal)
loader := atest.NewFileLoader()
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,
args: []string{"-p", simpleSuite, "--report", "md", "--report-file", path.Join(tmpFile.Name(), "fake")},
hasErr: true,
}, {
name: "malformed report file path",
prepare: fooPrepare,
args: []string{"-p", "[]]$#%*^&()"},
hasErr: true,
}}
for _, tt := range tests {
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) {
tests := []struct {
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
func Parse(configFile string) (testSuite *TestSuite, err error) {
var data []byte
if data, err = os.ReadFile(configFile); err == nil {
func Parse(data []byte) (testSuite *TestSuite, err error) {
testSuite, err = ParseFromData(data)
}
// schema validation
if err == nil {
@ -116,7 +113,7 @@ func (r *Request) Render(ctx interface{}, dataDir string) (err error) {
}
// setting default values
r.Method = emptyThenDefault(r.Method, http.MethodGet)
r.Method = EmptyThenDefault(r.Method, http.MethodGet)
return
}
@ -153,18 +150,20 @@ func (r *Request) GetBody() (reader io.Reader, err error) {
// Render renders the response
func (r *Response) Render(ctx interface{}) (err error) {
r.StatusCode = zeroThenDefault(r.StatusCode, http.StatusOK)
r.StatusCode = ZeroThenDefault(r.StatusCode, http.StatusOK)
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 {
val = defVal
}
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) == "" {
val = defVal
}

View File

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