白盒用例测试程序/go实现

简单介绍一下白盒测试:

    源码公开,清楚传参、返回值和处理逻辑;

我们在测试一个方法或者接口时,通过传入合法或者非法的参数,并且抽选一些具有代表性的值作为测试用的合法传参,通过模仿正常请求,检测方法或接口内部的异常。

周末没事,又想学习一下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`等问题。

发布了31 篇原创文章 · 获赞 3 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_36557960/article/details/103835347