chore: add release note v0.0.13 (#175)
This commit is contained in:
parent
bd78fa4868
commit
1ad11f38ef
5
Makefile
5
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:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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{
|
||||
|
@ -178,11 +212,13 @@ 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"`
|
||||
StringProp []StringProp `xml:"stringProp"`
|
||||
BoolProp []BoolProp `xml:"boolProp"`
|
||||
ElementProp []ElementProp `xml:"elementProp"`
|
||||
}
|
||||
|
||||
type ResultCollector struct {
|
||||
|
@ -201,6 +237,12 @@ type ElementProp struct {
|
|||
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 {
|
||||
|
|
|
@ -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`,
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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"}))
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue