簡單介紹一下白盒測試:
源碼公開,清楚傳參、返回值和處理邏輯;
我們在測試一個方法或者接口時,通過傳入合法或者非法的參數,並且抽選一些具有代表性的值作爲測試用的合法傳參,通過模仿正常請求,檢測方法或接口內部的異常。
週末沒事,又想學習一下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%來表示的...)
開發途中遇到協程的數量過多而導致鎖爭用超時和死鎖等問題。