白盒用例測試程序/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%來表示的...)

 

開發途中遇到協程的數量過多而導致鎖爭用超時和死鎖等問題。

發佈了34 篇原創文章 · 獲贊 3 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章