vislee / leevis.com

Blog
87 stars 13 forks source link

功能测试 #154

Open vislee opened 5 years ago

vislee commented 5 years ago

概述

最近重构了个系统,大大小小的功能大概有几十个,正常和例外的情况大概有上百个。虽然有单元测试,但是上了线上验证这么多case是一件很耗时很繁琐的事情。我希望有个工具能像单元测试的功能,把所有单元测试的用例在灰度的机器回放一遍,没有问题再镜像流量比对,或者导入小流量灰度几天,逐渐放量。

解决

我用golang写了个小工具,大概的流程是:

测试用例例子

[
    {
        "title": "Test01 redirect",
        "req": {
            "url": "/t/redirect/",
            "host": "www.test.com",
            "headers": {
               "x-request-type": "case_test"
            },
            "body":""
        },
        "resp": {
            "status": {
                "type": "contain",
                "value": "302"
            },
            "headers": [
                {
                    "key": "Location",
                    "type": "equal",
                    "value": "/test.html"
                }
            ],
            "body": {
                "type": "contain",
                "value": "302 Found"
            }
        }
    }
]

相关部分代码


func (self *matchKV) Match(v string) (bool, string) {
    msg := fmt.Sprintf("%s: got '%s', expected: '%s'",self.Key, v, self.Val)

    if (self.Val == "" || len(self.Val) == 0) && (self.Typ == "" || len(self.Typ) == 0) {
        return true, msg
    }

    if self.Typ == "contain" {
        return strings.Contains(v, self.Val), msg

    } else if self.Typ == "regex" {
        rp, err := regexp.Compile(self.Val)
        if err != nil {
            log.Fatalln(err.Error())
            return false, msg
        }

        return rp.Match([]byte(v)), msg
    }

    return self.Val == v, msg
}

type request struct {
    Timeout int64             `json:"timeout"`
    Url     string            `json:"url"`
    Host    string            `json:"host"`
    Addr   string             `json:"realAddr"`
    Headers map[string]string `json:"headers"`
    Body    string            `json:"body"`
}

func (self *request) newRequest(addr string) (*http.Request, error) {
    method := "GET"
    var b *bytes.Buffer = bytes.NewBufferString("")

    url := fmt.Sprintf("http://%s%s", addr, self.Url)

    if len(self.Body) > 0 {
        method = "POST"
        b = bytes.NewBufferString(self.Body)
    }

    req, err := http.NewRequest(method, url, b)
    if err != nil {
        return req, err
    }

    req.Host = self.Host

    for k, v := range self.Headers {
        req.Header.Add(k, v)
    }

    return req, nil
}

type response struct {
    Status  matchKV   `json:"status"`
    Headers []matchKV `json:"headers"`
    Body    matchKV   `json:"body"`
}

func (self *response) Match(resp *http.Response) bool {
    res := true

    if len(self.Status.Key) == 0 {
        self.Status.Key = "status"
    }
    if ok, msg := self.Status.Match(resp.Status); !ok {
        log.Println(msg)
        res = false
    }

    // resp.Header
    for _, header := range self.Headers {
        h := resp.Header.Get(header.Key)

        if ok, msg := header.Match(h); !ok {
            log.Println(msg)
            res = false
        }
    }

    // body
    data, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Printf("got resp body error. %s\n", err.Error())
        res = false
    } else {
        resp.Body.Close()
    }

    if len(self.Body.Key) == 0 {
        self.Body.Key = "body"
    }
    if ok, msg := self.Body.Match(string(data)); !ok {
        log.Println(msg)
        res = false
    }

    return res
}

type Case struct {
    Title  string   `json:"title"`
    Delay  int64    `json:"delay"`
    Repeat uint     `json:"repeat"`
    Req    request  `json:"req"`
    Resp   response `json:"resp"`
}

type HttpCli interface {
    Do(req *http.Request) (*http.Response, error)
    SetTimeout(d time.Duration)
    SetProxyProClientIP(remoteAddr string)
}

func (self *Case) Play(cli HttpCli, addr string) bool {
    var times uint = 0
    res := true

    time.Sleep(time.Duration(self.Delay) * time.Second)

    log.Printf("====[Title:%s][repeat:%d]====\n", self.Title, self.Repeat)

    req, err := self.Req.newRequest(addr)
    if err != nil {
        log.Println(err.Error())
        res = false
        goto endl
    }

    cli.SetTimeout(time.Duration(self.Req.Timeout))

    // cli.SetProxyProClientIP("127.0.0.1")
    // if len(self.Req.Addr) > 0 {
    //  cli.SetProxyProClientIP(self.Req.Addr)
    // }

    for {
        resp, err := cli.Do(req)
        if err != nil {
            log.Println(err.Error())
            res = false
            goto endl
        }
        if !self.Resp.Match(resp) {
            res = false
            goto endl
        }

        times = times + 1
        if times > self.Repeat {
            break
        }
    }

endl:
    ok := "OK"
    if !res {
        ok = "Field"
    }
    log.Printf("====[%s]====\n\n", ok)

    return res
}

type TestCases []Case

总结

通过工具把功能测试的配置文件生成两份文件, 其中一份是线上发布的测试配置,和测试功能相关。 另一份是测试流量回放文件,然后用该工具回放流量测试相关功能点。

当然,测试情况和架构类型强相关,希望能开动大脑把一些重复的繁琐的事情工具化。