最近在做项目设计的时候,考虑到实际项目与传统意义上的测试有些现实的差距。目前CI部分并不是对每次的测试环境都有一个新环境的准备,在实际的自动化测试中会糅合一部分QA人工的操作,在此背景下需要在自动化测试代码层面中控制,需要注意的是,e2e测试最好的设计还是能够有个干净的单独环境用于随时拉起与清除资源,以避免不要的脏数据保证与实际功能迭代相匹配 。
鉴于以上的上下文,顺便设计了个简单的可扩展的测试框架,便于项目集成。
功能拆分 [package]core
设计 关键元素拆分:
1 2 3 4 5 6 7 8 9 10 11 type BaseGroup struct { Name string Desc string Reqs []req.Req Client *restclient.Client }
group
抽象接口的设计:
1 2 3 4 5 6 7 8 9 10 type Interface interface { InitC() Init() Start() error Cleanup() error }
这里又要提一句的是,架构在设计 struct
的时候,尽量考虑复用。这里是因为在实际项目过程中,私有代码仓库中的 module
无法被引用,主要是因为历史原因在做整个大工程项目的时候没有做好很好的切分与独立,所以只能退而求其次来解决问题,考虑搞 sync
项目中的部分代码独立出一个库,但是这个高级功能在 gitlab
中需要付费版才支持,所以折中就出现如下架构设计。
1 2 3 4 5 6 7 8 9 10 11 12 13 type BaseReq struct { Name string Uri string Method string Body interface {} Query map [string ]string Runtime runtime.GroupRuntime }
1 2 3 4 5 6 7 8 9 10 11 12 type Req interface { Init() Do(c *restclient.Client) error Prepare() error } type PreReq interface { Init() PreCheck() interface {} }
这里抽象两个 req
,主要是为了便于处理资源准备接口的区分,在实际的资源准备中,对于请求之后的状态码其实不应该在设计的结构体中进行描述,为了更好的表意语义。
restclient:http
请求库构造,有了如上的测试元素的实例,还需要存在 http
客户端进行具体请求的执行,此次设计使用的 client
库resty ,虽然最近不维护了,但是看了下 star
和源码实现都是可控的。切记在选型时,要根据实际情况选择,如果社区活跃且支持较好,复杂点的库也可使用,如果社区活跃度一般,但是评估之后可控也可以适当尝试,具体看实际需求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 type Client struct { Rclient *resty.Client config.Config } func NewClient () *Client { conf := config.NewConfig() c := &Client{} c.Rclient = resty.New() c.Server = conf.Server c.Port = conf.Port c.User = conf.User c.Passwd = conf.Passwd c.Restries = conf.Restries return c } func (c *Client) Request (method, uri string , body interface {}, query map [string ]string ) (interface {}, error) { r := c.Rclient.R() r.SetHeader("Content-Type" , "application/json" ) token, err := c.getToken(r) if err != nil { return nil , err } t, err := util.GetValueFromJson(token, "token" ) if err != nil { return nil , err } r.SetAuthToken(t) url := fmt.Sprintf("http://%s:%s%s" , c.Server, c.Port, uri) switch method { case "GET" : resp, err := get(r, url, query) if err != nil { return nil , err } return resp, nil case "POST" : panic ("implement me" ) case "PUT" : panic ("implement me" ) case "DELETE" : panic ("implement me" ) default : return nil , fmt.Errorf("unsupported method: %s" , method) } } func (c *Client) getToken (r *resty.Request) (string , error) { var token string path := fmt.Sprintf("http://%s:%s/login" , c.Server, c.Port) resp, err := r.SetBody(map [string ]string {"userid" : "admin" , "password" : "Password" }). Post(path) if err != nil { return "" , fmt.Errorf("get token failed with gui web : %v" , err) } token = resp.String() return token, nil } func get (r *resty.Request, url string , query map [string ]string ) (interface {}, error) { var resp *resty.Response var err error if query != nil { resp, err = r.SetQueryParams(query). Get(url) if err != nil { return nil , fmt.Errorf("get request failed with gui web : %v" , err) } } else { resp, err = r.Get(url) if err != nil { return nil , fmt.Errorf("get request failed with gui web : %v" , err) } } return resp, nil }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 var c *Configtype Config struct { Server string `yaml:"server"` Port string `yaml:"port"` User string `yaml:"user"` Passwd string `yaml:"password"` Restries int `yaml:"restries"` } func NewConfig () *Config { dir, _ := os.Getwd() filePath := path.Join(dir, "config.yaml" ) config, err := ioutil.ReadFile(filePath) if err != nil { panic (err) } err = yaml.Unmarshal(config, &c) if err != nil { panic (err) } if c.Restries <= 0 { c.Restries = 3 } return c }
扩展设计与支持: 由于忙于工作其他项目的解决方案的设计,对于测试中间日志的显示和最终结果的汇总暂时还没做,这也是一个需要注意的点,先跳过此设计。
流程实现 既然具体功能元素已经拆分结束,下面就是需要设计入口来实现具体测试用例的注册与调用了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 type GuiApiTest struct { Groups []group.Interface } func NewGuiApiTest () *GuiApiTest { return &GuiApiTest{} } func (g *GuiApiTest) Init () { g.Groups = []group.Interface{ groups.NewFilesystemGroup(), } } func (g *GuiApiTest) Start () error { for i := range g.Groups { g.Groups[i].Init() if err := g.Groups[i].Start(); err != nil { return err } } return nil } func (g *GuiApiTest) Cleanup () error { for i := range g.Groups { if err := g.Groups[i].Cleanup(); err != nil { return err } } return nil }
继续往下看,看看 FilesystemGroup
中有啥?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 type GetFileSystemList struct { req.BaseReq prepares []req.PreReq StatusCode int } func (g *GetFileSystemList) Init () { g.Name = "Get_FileSystemList_Success" g.Url = "/v1/storage/filesystem/N9000" g.Method = "GET" g.StatusCode = 200 g.prepares = nil } func (g *GetFileSystemList) Prepare () error { return nil } func (g *GetFileSystemList) Do (c *restclient.Client) error { resp, err := c.Request(g.Method, g.Url, nil , nil ) if err != nil { return fmt.Errorf("[%s] test not passed" , g.Name) } statusCode := resp.(*resty.Response).StatusCode() if statusCode != g.StatusCode { return fmt.Errorf("[%s] test not passed. want status code: %d, got status code: %d" , g.Name, g.StatusCode, statusCode) } return nil }
如上 c.Request(g.Method, g.Url,nil,nil)
具体逻辑可以看 Client
的设计。
最后剩下的就是对整个入口的启动调用了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func main () { tests := pkg.NewGuiApiTest() tests.Init() err := tests.Start() if err != nil { os.Exit(1 ) } err = tests.Cleanup() if err != nil { os.Exit(1 ) } os.Exit(0 ) }
自此一个雏形设计就出来了,可在此基础上继续扩展实现实际的项目需求了。^_^