From 1ad11f38effc7bb7b1a2c69ef1b9b8f0d41e5d8d Mon Sep 17 00:00:00 2001 From: Rick <1450685+LinuxSuRen@users.noreply.github.com> Date: Tue, 22 Aug 2023 08:58:42 +0800 Subject: [PATCH] chore: add release note v0.0.13 (#175) --- Makefile | 5 +- cmd/convert.go | 4 +- docs/README.md | 8 ++ docs/release-note-v0.0.13.md | 68 +++++++++++++++++ pkg/generator/converter_jmeter.go | 76 ++++++++++++++----- pkg/generator/converter_jmeter_test.go | 3 +- pkg/generator/testdata/expected_jmeter.jmx | 14 +++- .../testdata/expected_testsuite.yaml | 3 +- pkg/testing/loader_file.go | 16 ++-- pkg/testing/parser.go | 20 +---- pkg/testing/parser_test.go | 33 -------- pkg/util/default.go | 42 ++++++++++ pkg/util/default_test.go | 33 ++++++++ pkg/util/map_test.go | 2 +- 14 files changed, 244 insertions(+), 83 deletions(-) create mode 100644 docs/release-note-v0.0.13.md diff --git a/Makefile b/Makefile index 2c9c499..9d3b2d4 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ IMG_TOOL?=podman +BINARY?=atest build: mkdir -p bin @@ -9,10 +10,12 @@ build-embed-ui: cp console/atest-ui/dist/index.html cmd/data/index.html cp console/atest-ui/dist/assets/*.js cmd/data/index.js cp console/atest-ui/dist/assets/*.css cmd/data/index.css - go build -ldflags "-w -s -X github.com/linuxsuren/api-testing/pkg/version.version=$(shell git rev-parse --short HEAD)" -o bin/atest main.go + GOOS=${OS} go build -ldflags "-w -s -X github.com/linuxsuren/api-testing/pkg/version.version=$(shell git rev-parse --short HEAD)" -o bin/${BINARY} main.go echo -n '' > cmd/data/index.html echo -n '' > cmd/data/index.js echo -n '' > cmd/data/index.css +build-win-embed-ui: + BINARY=atest.exe OS=windows make build-embed-ui goreleaser: goreleaser build --rm-dist --snapshot build-image: diff --git a/cmd/convert.go b/cmd/convert.go index f925e6b..6bfb6d1 100644 --- a/cmd/convert.go +++ b/cmd/convert.go @@ -64,9 +64,7 @@ type convertOption struct { } func (o *convertOption) preRunE(c *cobra.Command, args []string) (err error) { - if o.target == "" { - o.target = "sample.jmx" - } + o.target = util.EmptyThenDefault(o.target, "sample.jmx") return } diff --git a/docs/README.md b/docs/README.md index 1eece8e..c8210b1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -35,6 +35,13 @@ Please see the following example usage: sudo atest service install -m podman --version master ``` +or run in Docker: +```shell +docker run -v /var/www/sample:/var/www/sample \ + --network host \ + linuxsuren/api-testing:master +``` + the default web server port is `8080`. So you can visit it via: http://localhost:8080 ## Run in k3s @@ -203,6 +210,7 @@ You could find the official images from both [Docker Hub](https://hub.docker.com The tag `latest` represents the latest release version. The tag `master` represents the image of the latest master branch. We highly recommend you using a fixed version instead of those in a production environment. ## Release Notes +* [v0.0.13](release-note-v0.0.13.md) * [v0.0.12](release-note-v0.0.12.md) ## Articles diff --git a/docs/release-note-v0.0.13.md b/docs/release-note-v0.0.13.md new file mode 100644 index 0000000..c2ca1d1 --- /dev/null +++ b/docs/release-note-v0.0.13.md @@ -0,0 +1,68 @@ +`atest` 版本发布 `v0.0.13` + +`atest` 是一款用 Golang 编写的、开源的接口测试工具。 + +你可以在容器中启动: + +```shell +docker run -v /var/www/sample:/var/www/sample \ + --network host \ + linuxsuren/api-testing:master +``` + +或者,直接[下载二进制文件](https://github.com/LinuxSuRen/api-testing/releases/tag/v0.0.12)后启动: + +```shell +atest server --local-storage /var/www/sample +``` + +对于持续集成(CI)场景,可以通过在流水线中执行命令的方式: + +```shell +# 执行本地文件 +atest run -p your-test-suite.yaml +# 执行远程文件 +atest run -p https://gitee.com/linuxsuren/api-testing/raw/master/sample/testsuite-gitee.yaml +# 容器中执行 +docker run linuxsuren/api-testing:master atest run -p https://gitee.com/linuxsuren/api-testing/raw/master/sample/testsuite-gitee.yaml +``` + +你也可以把测试用例转为 JMeter 文件并执行: + +```shell +# 格式转换 +atest convert --converter jmeter -p https://gitee.com/linuxsuren/api-testing/raw/master/sample/testsuite-gitee.yaml --target gitee.jmx +# 执行 +jmeter -n -t gitee.jmx +``` + +## 主要的新功能 + +* 增加了插件扩展机制,支持以 Git、S3、关系型数据为后端存储,支持从 [Vault](https://github.com/hashicorp/vault) 获取密码等敏感信息 +* 新增对 gRPC 接口的用例支持 @Ink-33 +* 支持导出 [JMeter](https://github.com/apache/jmeter) 文件 +* 支持通过 [Operator](https://operatorhub.io/operator/api-testing-operator) 的方式安装,并上架 OperatorHub.io +* 提供了基本的 Web UI +* 支持导出 PDF 格式的测试报告 @wjsvec + +本次版本发布,包含了以下 5 位 contributor 的努力: + +* [@Ink-33](https://github.com/Ink-33) +* [@LinuxSuRen](https://github.com/LinuxSuRen) +* [@chan158](https://github.com/chan158) +* [@setcy](https://github.com/setcy) +* [@wjsvec](https://github.com/wjsvec) + +## 相关数据 + +下面是 `atest` 截止到 `v0.0.13` 的部分数据: + +* watch 7 +* fork 18 +* star 69 +* contributor 8 +* 二进制文件下载量 872 +* 代码行数 45k +* 单元测试覆盖率 84% + +想了解完整信息的话,请访问 https://github.com/LinuxSuRen/api-testing/releases/tag/v0.0.13 diff --git a/pkg/generator/converter_jmeter.go b/pkg/generator/converter_jmeter.go index 7692cc4..ec437c1 100644 --- a/pkg/generator/converter_jmeter.go +++ b/pkg/generator/converter_jmeter.go @@ -26,6 +26,7 @@ package generator import ( "encoding/xml" + "fmt" "net/url" "github.com/linuxsuren/api-testing/pkg/testing" @@ -50,27 +51,31 @@ func (c *jmeterConverter) Convert(testSuite *testing.TestSuite) (result string, } func (c *jmeterConverter) buildJmeterTestPlan(testSuite *testing.TestSuite) (result *JmeterTestPlan, err error) { - if err = testSuite.Render(make(map[string]interface{})); err != nil { + emptyCtx := make(map[string]interface{}) + if err = testSuite.Render(emptyCtx); err != nil { return } requestItems := []interface{}{} for _, item := range testSuite.Items { item.Request.RenderAPI(testSuite.API) + if reqRenderErr := item.Request.Render(emptyCtx, ""); reqRenderErr != nil { + fmt.Println("Error rendering request: ", reqRenderErr) + } api, err := url.Parse(item.Request.API) if err != nil { continue } - requestItems = append(requestItems, &HTTPSamplerProxy{ + requestItem := &HTTPSamplerProxy{ GUIClass: "HttpTestSampleGui", TestClass: "HTTPSamplerProxy", Enabled: true, Name: item.Name, StringProp: []StringProp{{ Name: "HTTPSampler.domain", - Value: api.Host, + Value: api.Hostname(), }, { Name: "HTTPSampler.port", Value: api.Port(), @@ -81,7 +86,36 @@ func (c *jmeterConverter) buildJmeterTestPlan(testSuite *testing.TestSuite) (res Name: "HTTPSampler.method", Value: item.Request.Method, }}, - }) + } + if item.Request.Body != "" { + requestItem.BoolProp = append(requestItem.BoolProp, BoolProp{ + Name: "HTTPSampler.postBodyRaw", + Value: "true", + }) + requestItem.ElementProp = append(requestItem.ElementProp, ElementProp{ + Name: "HTTPsampler.Arguments", + Type: "Arguments", + CollectionProp: []CollectionProp{{ + Name: "Arguments.arguments", + ElementProp: []ElementProp{{ + Name: "", + Type: "HTTPArgument", + BoolProp: []BoolProp{{ + Name: "HTTPArgument.always_encode", + Value: "false", + }}, + StringProp: []StringProp{{ + Name: "Argument.value", + Value: item.Request.Body, + }, { + Name: "Argument.metadata", + Value: "=", + }}, + }}, + }}, + }) + } + requestItems = append(requestItems, requestItem) requestItems = append(requestItems, HashTree{}) } requestItems = append(requestItems, &ResultCollector{ @@ -177,12 +211,14 @@ type ThreadGroup struct { } type HTTPSamplerProxy struct { - XMLName xml.Name `xml:"HTTPSamplerProxy"` - StringProp []StringProp `xml:"stringProp"` - Name string `xml:"testname,attr"` - GUIClass string `xml:"guiclass,attr"` - TestClass string `xml:"testclass,attr"` - Enabled bool `xml:"enabled,attr"` + XMLName xml.Name `xml:"HTTPSamplerProxy"` + Name string `xml:"testname,attr"` + GUIClass string `xml:"guiclass,attr"` + TestClass string `xml:"testclass,attr"` + Enabled bool `xml:"enabled,attr"` + StringProp []StringProp `xml:"stringProp"` + BoolProp []BoolProp `xml:"boolProp"` + ElementProp []ElementProp `xml:"elementProp"` } type ResultCollector struct { @@ -194,13 +230,19 @@ type ResultCollector struct { } type ElementProp struct { - Name string `xml:"name,attr"` - Type string `xml:"elementType,attr"` - GUIClass string `xml:"guiclass,attr"` - TestClass string `xml:"testclass,attr"` - Enabled bool `xml:"enabled,attr"` - StringProp []StringProp `xml:"stringProp"` - BoolProp []BoolProp `xml:"boolProp"` + Name string `xml:"name,attr"` + Type string `xml:"elementType,attr"` + GUIClass string `xml:"guiclass,attr"` + TestClass string `xml:"testclass,attr"` + Enabled bool `xml:"enabled,attr"` + StringProp []StringProp `xml:"stringProp"` + BoolProp []BoolProp `xml:"boolProp"` + CollectionProp []CollectionProp `xml:"collectionProp"` +} + +type CollectionProp struct { + Name string `xml:"name,attr"` + ElementProp []ElementProp `xml:"elementProp"` } type StringProp struct { diff --git a/pkg/generator/converter_jmeter_test.go b/pkg/generator/converter_jmeter_test.go index 5a9fda1..2809b67 100644 --- a/pkg/generator/converter_jmeter_test.go +++ b/pkg/generator/converter_jmeter_test.go @@ -52,12 +52,13 @@ func TestJmeterConvert(t *testing.T) { func createTestSuiteForTest() *atest.TestSuite { return &atest.TestSuite{ Name: "API Testing", - API: "http://localhost:8080", + API: `{{default "http://localhost:8080/server.Runner" (env "SERVER")}}`, Items: []atest.TestCase{{ Name: "hello-jmeter", Request: atest.Request{ Method: "POST", API: "/GetSuites", + Body: `sample`, }, }}, } diff --git a/pkg/generator/testdata/expected_jmeter.jmx b/pkg/generator/testdata/expected_jmeter.jmx index 447da3c..441fc9b 100644 --- a/pkg/generator/testdata/expected_jmeter.jmx +++ b/pkg/generator/testdata/expected_jmeter.jmx @@ -13,10 +13,20 @@ - localhost:8080 + localhost 8080 - /GetSuites + /server.Runner/GetSuites POST + true + + + + sample + = + false + + + diff --git a/pkg/generator/testdata/expected_testsuite.yaml b/pkg/generator/testdata/expected_testsuite.yaml index f1525b6..3d7b606 100644 --- a/pkg/generator/testdata/expected_testsuite.yaml +++ b/pkg/generator/testdata/expected_testsuite.yaml @@ -1,7 +1,8 @@ name: API Testing -api: http://localhost:8080 +api: http://localhost:8080/server.Runner items: - name: hello-jmeter request: api: /GetSuites method: POST + body: sample diff --git a/pkg/testing/loader_file.go b/pkg/testing/loader_file.go index a559fa1..415c96a 100644 --- a/pkg/testing/loader_file.go +++ b/pkg/testing/loader_file.go @@ -36,12 +36,17 @@ func NewFileWriter(parent string) Writer { // HasMore returns if there are more test cases func (l *fileLoader) HasMore() bool { l.index++ - return l.index < len(l.paths) + return l.index < len(l.paths) && l.index >= 0 } // Load returns the test case content func (l *fileLoader) Load() (data []byte, err error) { targetFile := l.paths[l.index] + data, err = loadData(targetFile) + return +} + +func loadData(targetFile string) (data []byte, err error) { if strings.HasPrefix(targetFile, "http://") || strings.HasPrefix(targetFile, "https://") { var ok bool data, ok, err = gRPCCompitableRequest(targetFile) @@ -132,14 +137,13 @@ func (l *fileLoader) Reset() { } func (l *fileLoader) ListTestSuite() (suites []TestSuite, err error) { - defer func() { - l.Reset() - }() + l.lock.RLocker().Lock() + defer l.lock.RUnlock() - for l.HasMore() { + for _, target := range l.paths { var data []byte var loadErr error - if data, loadErr = l.Load(); err != nil { + if data, loadErr = loadData(target); err != nil { fmt.Println("failed to load data", loadErr) continue } diff --git a/pkg/testing/parser.go b/pkg/testing/parser.go index edb6cde..3b61515 100644 --- a/pkg/testing/parser.go +++ b/pkg/testing/parser.go @@ -156,7 +156,7 @@ func (r *Request) Render(ctx interface{}, dataDir string) (err error) { } // setting default values - r.Method = EmptyThenDefault(r.Method, http.MethodGet) + r.Method = util.EmptyThenDefault(r.Method, http.MethodGet) return } @@ -201,7 +201,7 @@ 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 = util.ZeroThenDefault(r.StatusCode, http.StatusOK) return } @@ -217,19 +217,3 @@ func renderMap(ctx interface{}, data map[string]string, title string) (result ma result = data return } - -// ZeroThenDefault return the default value if the val is zero -func ZeroThenDefault(val, defVal int) int { - if val == 0 { - val = defVal - } - return val -} - -// EmptyThenDefault return the default value if the val is empty -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 816aca1..0ceb15d 100644 --- a/pkg/testing/parser_test.go +++ b/pkg/testing/parser_test.go @@ -230,39 +230,6 @@ func TestResponseRender(t *testing.T) { } } -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 := atest.EmptyThenDefault(tt.val, tt.defVal) - assert.Equal(t, tt.expect, result, result) - }) - } - - assert.Equal(t, 1, atest.ZeroThenDefault(0, 1)) - assert.Equal(t, 1, atest.ZeroThenDefault(1, 2)) -} - func TestTestCase(t *testing.T) { testCase, err := atest.ParseTestCaseFromData([]byte(testCaseContent)) assert.Nil(t, err) diff --git a/pkg/util/default.go b/pkg/util/default.go index 5f6cfa2..0261791 100644 --- a/pkg/util/default.go +++ b/pkg/util/default.go @@ -1,6 +1,32 @@ +/** +MIT License + +Copyright (c) 2023 API Testing Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + // Package util provides a set of common functions package util +import "strings" + // MakeSureNotNil makes sure the parameter is not nil func MakeSureNotNil[T any](inter T) T { switch val := any(inter).(type) { @@ -20,6 +46,22 @@ func MakeSureNotNil[T any](inter T) T { return inter } +// ZeroThenDefault return the default value if the val is zero +func ZeroThenDefault(val, defVal int) int { + if val == 0 { + val = defVal + } + return val +} + +// EmptyThenDefault return the default value if the val is empty +func EmptyThenDefault(val, defVal string) string { + if strings.TrimSpace(val) == "" { + val = defVal + } + return val +} + // ContentType is the HTTP header key const ( ContentType = "Content-Type" diff --git a/pkg/util/default_test.go b/pkg/util/default_test.go index a8c0319..124493f 100644 --- a/pkg/util/default_test.go +++ b/pkg/util/default_test.go @@ -16,3 +16,36 @@ func TestMakeSureNotNil(t *testing.T) { assert.NotNil(t, util.MakeSureNotNil(mapStruct)) assert.NotNil(t, util.MakeSureNotNil(map[string]string{})) } + +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 := util.EmptyThenDefault(tt.val, tt.defVal) + assert.Equal(t, tt.expect, result, result) + }) + } + + assert.Equal(t, 1, util.ZeroThenDefault(0, 1)) + assert.Equal(t, 1, util.ZeroThenDefault(1, 2)) +} diff --git a/pkg/util/map_test.go b/pkg/util/map_test.go index 7af0ee6..d43a795 100644 --- a/pkg/util/map_test.go +++ b/pkg/util/map_test.go @@ -33,7 +33,7 @@ import ( func TestKeys(t *testing.T) { t.Run("normal", func(t *testing.T) { - assert.Equal(t, []string{"foo", "bar"}, + assert.ElementsMatch(t, []string{"foo", "bar"}, util.Keys(map[string]interface{}{"foo": "xx", "bar": "xx"})) }) }