chore: add release note v0.0.13 (#175)

This commit is contained in:
Rick 2023-08-22 08:58:42 +08:00 committed by GitHub
parent bd78fa4868
commit 1ad11f38ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 244 additions and 83 deletions

View File

@ -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:

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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`,
},
}},
}

View File

@ -13,10 +13,20 @@
</ThreadGroup>
<hashTree>
<HTTPSamplerProxy testname="hello-jmeter" guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" enabled="true">
<stringProp name="HTTPSampler.domain">localhost:8080</stringProp>
<stringProp name="HTTPSampler.domain">localhost</stringProp>
<stringProp name="HTTPSampler.port">8080</stringProp>
<stringProp name="HTTPSampler.path">/GetSuites</stringProp>
<stringProp name="HTTPSampler.path">/server.Runner/GetSuites</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="" testclass="" enabled="false">
<collectionProp name="Arguments.arguments">
<elementProp name="" elementType="HTTPArgument" guiclass="" testclass="" enabled="false">
<stringProp name="Argument.value">sample</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
<boolProp name="HTTPArgument.always_encode">false</boolProp>
</elementProp>
</collectionProp>
</elementProp>
</HTTPSamplerProxy>
<hashTree></hashTree>
<ResultCollector enabled="true" guiclass="SummaryReport" testclass="ResultCollector" testname="Summary Report"></ResultCollector>

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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"

View File

@ -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))
}

View File

@ -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"}))
})
}