简单介绍一下白盒测试:
源码公开,清楚传参、返回值和处理逻辑;
我们在测试一个方法或者接口时,通过传入合法或者非法的参数,并且抽选一些具有代表性的值作为测试用的合法传参,通过模仿正常请求,检测方法或接口内部的异常。
周末没事,又想学习一下go,最近又老写bug,所以就想到用go写一个测试进程,实现白盒`用例`测试
用到的数据结构简单介绍一哈:
用Clinet表示一个正常的客服端, ClientPool是一个Client连接池,复用客户端与服务端的套接口连接(即在http请求头中包含 `connection: keepAlive`),减少了端口的开销,就可以实现100w的请求量;由于是在本地开启的服务端和客户端,如果使用短连接的话,将会有许多套接口处于`TIME_WAIT`状态,多到再无可用端口,客户端(如将`MaxConnsPerHost`置为小于0的数值,当一次请求完成后,客服端就会主动关闭套接口)和服务端(如将`DisableKeepAlive` 为 false ,那么一定时间内客服端都没有发送消息给服务端,服务端将会主动关闭套接口;而将`DisableKeepAlive` 为 true的话,服务端也会在http请求应答发送完毕后,主动关闭连接)的正常连接会受到影响从而影响了测试(主动关闭的套接口状态会转为`TIME_WAIT`,一般情况下,在`2ML时间内`,该套接口绑定的端口就暂不可用),所以为了不影响测试而复用socket套接口。
在net/http的实现中,只有当真正发送了http请求(因为发送http请求时才会给出 `host`)才会连接服务端,如果连接池中有可复用(同一`host`下)的连接则会复用连接;
`测试用例` 所用的数据结构:
//包装了请求的response的body和code,并记录了用时
type Response struct {
Code int //请求成功
Response string //返回值
Timestamp time.Duration //耗时
}
//一次测试的数据统计
type TD struct {
Tg *TG //
Cost time.Duration //用时
Succ int //请求成功的次数
Fail int //请求失败的次数
Response []Response //请求结果
}
//一个测试用例,进程中并没有处理传参的具体类型,只是json_decode又json_encode而已
type TInput struct {
Params map[string]interface{} `json:params`
}
//解析输入测试文件, 解析输出结果
type TG struct {
Url string `json:url` //请求接口(地址)
Cnt int `json:cnt` //请求数量
Ret string `json:result` //期望的返回值
List []TInput `json:list` //测试用例集合
}
`客服端` 所用的数据结构
//简单封装了 http.Client
type Client struct {
busy chan byte //记录当前client有几个conn处于忙碌
obj *http.Client
}
//Client连接池
type ClientPool struct {
config ClientConfig //http.client配置项
cnt int //连接池数量
freeCnt int //连接池空闲连接数量
freePersistentQue []Client //空闲长连接client队列
}
完整程序:
package httpClientPool
import (
"net/http"
)
//创建一个client,获取一个client,销毁一个client,对client集合进行迭代
type ClientConfig struct {
MaxIdleConnCnt int //最大连接数量, 限制了最大连接数量
PerHostConnSize int //每一个host保持的连接数量
DisableKeepAlive bool //http.client连接复用
}
//简单封装了 http.Client
type Client struct {
busy chan byte //记录当前client有几个conn处于忙碌
obj *http.Client
}
//Client连接池
type ClientPool struct {
config ClientConfig //http.client配置项
cnt int //连接池数量
freeCnt int //连接池空闲连接数量
freePersistentQue []Client //空闲长连接client队列
}
//创造一个client
func NewClient(config ClientConfig) Client {
tr := &http.Transport{
MaxIdleConns: config.MaxIdleConnCnt,
MaxConnsPerHost: config.PerHostConnSize,
DisableKeepAlives: config.DisableKeepAlive,
}
hc := &http.Client{
Transport: tr,
}
return Client{
obj: hc,
busy: make(chan byte, config.PerHostConnSize),
}
}
//获取一个client, err暂时为nil, ok暂时为true
func (cp *ClientPool) Get() (client *Client, err error) {
client, ok := cp.pop()
if !ok {
//创建一个短连接
nc := NewClient(ClientConfig{
MaxIdleConnCnt: 1, //短连接有效连接数量
PerHostConnSize: -1, //让客服端主动断开连接
DisableKeepAlive: false, //让服务端保持连接, 有客服端断开连接
})
client := &nc
return client, err
}
return
}
//创建一个新的Client客服端连接池
func NewClientPool(maxIdleConn, connSizePerHost int, disableKeepAlive bool, clientCnt int) *ClientPool {
config := ClientConfig{
MaxIdleConnCnt: maxIdleConn,
PerHostConnSize: connSizePerHost,
DisableKeepAlive: disableKeepAlive,
}
freeClientQue := make([]Client,0, clientCnt)
for i := 0; i < clientCnt; i++ {
freeClientQue = append(freeClientQue, NewClient(config))
}
return &ClientPool{
config: config,
cnt: clientCnt,
freeCnt: clientCnt,
freePersistentQue: freeClientQue,
}
}
func (cp *ClientPool) pop() (client *Client, ok bool) {
//在创建连接池时, clientQueue就完成了初始化, 并且后面从clientPool中取元素时, 也是在一个死循环中, 即连接池大小固定
ok = true
//这里不考虑对资源`item.busy`的并发竞争
outer:
for {
for _, item := range cp.freePersistentQue {
//寻在一个不忙的客户端
select {
case <- item.busy:
continue
default:
client = &item
break outer
}
}
}
return
}
//对client集合进行回收
func (cp *ClientPool) gc() {
}
//删除client
func (c *Client) del() {
c.obj.CloseIdleConnections() //关闭所有空闲连接
}
package httpClientPool
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"sync"
"time"
)
const GO_UP = 4095
const (
REQUEST_SUCC = iota+1
REQUEST_FAIL
)
type Response struct {
Code int //请求成功
Response string //返回值
Timestamp time.Duration //耗时
}
type TD struct {
Tg *TG //
Cost time.Duration //用时
Succ int //请求成功的次数
Fail int //请求失败的次数
Response []Response //请求结果
}
//测试样例
type TInput struct {
Params map[string]interface{} `json:params`
}
//解析输入测试文件, 解析输出结果
type TG struct {
Url string `json:url`
Cnt int `json:cnt`
Ret string `json:result`
List []TInput `json:list`
}
//解析测试用例
func readCase(fileName string) (tg *TG, err error) {
file, err := os.OpenFile(fileName, os.O_RDONLY, 0755)
if err != nil {
err = fmt.Errorf("[readCase]打开文件失败 `%s`;%v", fileName, err)
return
}
defer file.Close()
content, err := ioutil.ReadAll(file)
if err != nil {
err = fmt.Errorf("[readCase]获取文件内容失败 `%s`;%v", fileName, err)
}
tg = &TG{
}
err = json.Unmarshal(content, tg)
if err != nil {
err = fmt.Errorf("[readCase]json解析失败 `%s`;%v", fileName, err)
}
return
}
//主流程
func Process(fileName string) (td *TD, err error) {
//解析白盒测试用例
tg, err := readCase(fileName)
if err != nil {
err = fmt.Errorf("测试用例解析失败, %v", err)
return
}
cp := NewClientPool(48, 2, false, 24)
wg := sync.WaitGroup{}
gwg := sync.WaitGroup{}
td = &TD{
Tg: tg,
}
work := func(client *Client, td *TD, post []byte) {
mutex := sync.Mutex{}
ts := time.Now()
//避免并发竞争下引起的阻塞, 而导致进程一直阻塞
select {
case client.busy <- 1:
default:
break;
}
resp, err := client.obj.Post(td.Tg.Url, "application/json", bytes.NewBufferString(string(post)))
if err != nil {
return
}
cost := time.Since(ts).Milliseconds()
cod := REQUEST_SUCC
//存在`并发竞争`情况, 一般情况下, 请求成功的概率高于失败, 所以只统计失败次数, 而成功次数由请求总数-失败次数
retBody, err := ioutil.ReadAll(resp.Body)
mutex.Lock()
if err != nil || resp.StatusCode != 200 || string(retBody) != td.Tg.Ret {
cod = REQUEST_FAIL
retBody = []byte("fail")
td.Fail++
}
mutex.Unlock()
ret := Response{
Code: cod,
Timestamp: time.Duration(cost),
Response: string(retBody),
}
td.Response = append(td.Response, ret)
wg.Add(-1)
gwg.Add(-1)
}
ts := time.Now()
var j = 0
for i := 0; i < tg.Cnt; i++ {
//协程数量存在上限, 这里将请求分片
if j&GO_UP == GO_UP {
j = 0
gwg.Wait()
}
for ca := range tg.List {
j++
client, err := cp.Get()
if err != nil {
fmt.Printf("get from pool, err:`%v`\n", err)
continue
}
jsonData, err := json.Marshal(ca)
if err != nil {
fmt.Printf("conver to json, err:`%v`\n", err)
continue
}
gwg.Add(1)
wg.Add(1)
go work(client, td, jsonData)
}
}
wg.Wait()
te := time.Since(ts)
td.Succ = td.Tg.Cnt - td.Fail
td.Cost = te
return
}
测试demo:
package main
import (
"fmt"
"net/http"
)
func sayOk(w http.ResponseWriter, r *http.Request) {
fmt.Sprint(w, "success")
}
func main() {
http.HandleFunc("/", sayOk)
http.ListenAndServe(":8080", nil)
}
package main
import (
"dora/httpClientPool"
"fmt"
)
func main() {
result, err := httpClientPool.Process("case.json")
if err != nil {
fmt.Printf("测试失败, err:`%v`\n", err)
} else {
fmt.Printf("测试成功, info:[request:`%d`, cost:`%v`, success:`%d`, fail:`%d`]\n", result.Tg.Cnt, result.Cost, result.Succ, result.Fail)
}
}
测试用例文件
{
"url": "http://localhost:8080/",
"cnt": 10000,
"result": "success",
"list": [
{
"params": {
"user": "dora",
"age": 13
}
}
]
}
程序效率计算:
1w请求, 用时在1s左右, 成功率99.99%
10w请求,用时在10s左右, 成功率99.99%
100w请求,用时在1m30s左右, 成功率99.99%(想用100%来表示的...)
开发途中遇到,go的数量过多而引发关于 栈 的问题和锁争用`有的协程等待时间超过1m`等问题。