接口自动化测试平台-LRUN


为什么要写测试平台

从事测试行业也有几年了,也用过很多的测试平台。之前看到过一位大佬说,当你从事测试几年后,你是想当一个工具的制造者还是做一个使用者。我想,我是想当一个制造者。 

2020第四季度的时候使用 Django + DRF + vue2 + HttpRunner2.x 写过一个半成品的接口自动化平台,后面由于一些原因就没有在写。直到 2021 年的第四季度,由于一些原因,又开始搞接口自动化测试平台了。

这次前端用的技术栈是 vue3 的 Composition API 语法(基于最新的 3.2.x 版本的 script setup 语法)、typescript、vite,element-plus 以及 vue3 的全家桶。

为什么要用 vue3 和 ts 当然是更快更好用,composition-api 更加适合大项目,ts 的可靠性和可维护性更高,虽然可能我用不到这些特性,但并不影响我以后出去面试的时候吹牛逼。

后端使用的是 Python 的 FastAPI 框架 + sqlalchemy + mysql + Apscheduler + pydantic 等

最早看到 FastAPI 框架是在 HttpRunner3.x 的源码里看到的,当时读源码的时候不知道这个库是做什么的,然后就去 Google 上搜索了一下。当时网上关于 FastAPI 的资料还是比较少的,就去看了下官网,从刚开始打算了解一下,到花了一下午的时间,都看啃官网文档。看完官方文档之后就放弃用了几年的 Django 框架。

平台大体可以分为三个部分。第一部分也是最主要的。运行 api 并且返回测试结果。我起名为 LRUN,L 代表了我姓的首字母和公司英文的首字母。它的主要功能就是传入一个 yaml 路径,在内部对 yaml 文件进行处理,包括参数提取、参数替换、断言、hooks,最后返回测试结果。

例如下面的 yaml 内容,实现了四个接口。登录接口,登录成功后获取到 token,保存为变量,在传给后面的接口,可以在请求头里使用提取到的变量。新增供应商,字段 contacts 是不能重复的,所以我写了个随机生成名称的方法,在这里直接调用这个方法。也可以选择该接口是否执行、执行次数、请求前置、请求后置等

- config:
    base_url: http://47.101.111.187:8081
    headers: {}
    name: 冒烟测试
    variables: {}
- test:
    extract:
      code: body.code
      token: body.data.token
    mock: false
    name: 登录接口
    request:
      headers:
        Accept-Language: zh-cn
      json:
        password: zouzou
        username: zouzou
      method: POST
      url: /api/user/login
    setup_hooks:
    - ${hook_down()}
    - ${hook_down()}
    skip: false
    ssl: true
    teardown_hooks:
    - ${hook_down()}
    times: 1
    validate:
    - eq:
      - body.code
      - '2001'
    - eq:
      - body.msg
      - login success
    - eq:
      - status_code
      - 200
- test:
    extract: {}
    mock: false
    name: 新增供应商
    request:
      headers:
        Authorization: JWT ${token}
      json:
        contacts: ${staff_name()}
        contacts_iphone: '17111111111'
        id: null
        remarks: '12'
        supplier_name: ${staff_name()}
      method: PUT
      url: /api/manage/supplier
    skip: false
    ssl: false
    times: 1
    validate:
    - ne:
      - body.msg
      - 供应商添加成功
- test:
    extract:
      id: body.data[0].id
    mock: false
    name: 查询员工数据
    request:
      headers:
        Authorization: JWT ${token}
      json:
        account: ''
        name: ''
      method: POST
      url: /api/manage/search/supplier?page=1&size=10
    skip: false
    ssl: true
    times: 1
    validate:
    - eq:
      - body.code
      - '2001'
    - eq:
      - status_code
      - 200
    - len_gt:
      - body.data
      - 8
- test:
    extract: {}
    mock: false
    name: 删除供应商
    request:
      headers:
        Authorization: JWT ${token}
      json: {}
      method: DELETE
      url: /api/manage/supplier?id=${id}
    skip: false
    ssl: false
    times: 1
    validate:
    - eq:
      - body.msg
      - 删除成功

各参数意义如下

  • ?times:接口运行次数

  • skip:是否跳过接口

  • SSL:是否开启 SSL 验证。

  • mock:是否 mock 接口(目前功能还未实现)

  • request:填写请求信息,请求方式、请求体、path 等

  • extract:提取接口响应里的参数

  • validate:断言,支持丰富的断言方式(13种断言方式,满足接口测试断言的多种场景)

  • setup_hooks:请求前置,可以做一些加密处理

  • teardown_hooks:请求后置

运行上面的 yaml 文件后返回的结果如下,会返总耗时、总接口数、成功用例数、失败用例数、跳过的用例数。还有单个接口的详细信息。

{
    "name":"冒烟测试",
    "total":4,
    "case_id":"adaad59f-af5f-409e-87e1-d621a97f7985",
    "success":false,
    "success_total":3,
    "fail_total":1,
    "total_time":0.156,
    "skip":0,
    "step_data":[
        {
            "details":[
                {
                    "results":{
                        "name":"登录接口",
                        "success":true,
                        "time":"2021-12-30 22:48:38",
                        "duration_ms":23.52,
                        "content_size":192
                    }
                },
                {
                    "request":{
                        "url":"http://47.101.111.187:8081/api/user/login",
                        "headers":{
                            "User-Agent":"python-requests/2.26.0",
                            "Accept-Encoding":"gzip, deflate",
                            "Accept":"*/*",
                            "Connection":"keep-alive",
                            "Accept-Language":"zh-cn",
                            "Content-Length":"44",
                            "Content-Type":"application/json"
                        },
                        "method":"POST",
                        "body":{
                            "password":"zouzou",
                            "username":"zouzou"
                        }
                    }
                },
                {
                    "response":{
                        "status_code":200,
                        "reason":"OK",
                        "headers":{
                            "Connection":"close",
                            "Content-Length":"192",
                            "Allow":"POST, OPTIONS",
                            "Content-Type":"application/json",
                            "Date":"Thu, 30 Dec 2021 14:48:38 GMT",
                            "Referrer-Policy":"same-origin",
                            "Server":"nginx/1.16.1",
                            "Vary":"Accept, Origin, Cookie",
                            "X-Content-Type-Options":"nosniff",
                            "X-Frame-Options":"DENY"
                        },
                        "cookies":{
?
                        },
                        "body":{
                            "code":"2001",
                            "success":true,
                            "msg":"login success",
                            "data":{
                                "token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImV4cCI6MTY0MTQ4MDUxOH0.nipBF4Ov1QgofPx7lGgoDPlS9_saWUbSpFUAzZfimts"
                            }
                        }
                    }
                },
                {
                    "validate":{
                        "validate_extractor":[
                            {
                                "comparator":"equal",
                                "check":"body.code",
                                "check_value":"2001",
                                "expect_value":"2001",
                                "check_result":"pass"
                            },
                            {
                                "comparator":"equal",
                                "check":"body.msg",
                                "check_value":"login success",
                                "expect_value":"login success",
                                "check_result":"pass"
                            },
                            {
                                "comparator":"equal",
                                "check":"status_code",
                                "check_value":200,
                                "expect_value":200,
                                "check_result":"pass"
                            }
                        ]
                    }
                }
            ]
        },
        {
            "details":[
                {
                    "results":{
                        "name":"新增供应商",
                        "success":true,
                        "time":"2021-12-30 22:48:38",
                        "duration_ms":23.18,
                        "content_size":58
                    }
                },
                {
                    "request":{
                        "url":"http://47.101.111.187:8081/api/manage/supplier",
                        "headers":{
                            "User-Agent":"python-requests/2.26.0",
                            "Accept-Encoding":"gzip, deflate",
                            "Accept":"*/*",
                            "Connection":"keep-alive",
                            "Authorization":"JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImV4cCI6MTY0MTQ4MDUxOH0.nipBF4Ov1QgofPx7lGgoDPlS9_saWUbSpFUAzZfimts",
                            "Content-Length":"136",
                            "Content-Type":"application/json"
                        },
                        "method":"PUT",
                        "body":{
                            "contacts":"测试204260",
                            "contacts_iphone":"17111111111",
                            "id":null,
                            "remarks":"12",
                            "supplier_name":"测试204260"
                        }
                    }
                },
                {
                    "response":{
                        "status_code":200,
                        "reason":"OK",
                        "headers":{
                            "Connection":"close",
                            "Content-Length":"58",
                            "Allow":"GET, POST, PUT, DELETE, HEAD, OPTIONS",
                            "Content-Type":"application/json",
                            "Date":"Thu, 30 Dec 2021 14:48:38 GMT",
                            "Referrer-Policy":"same-origin",
                            "Server":"nginx/1.16.1",
                            "Vary":"Accept, Origin",
                            "X-Content-Type-Options":"nosniff",
                            "X-Frame-Options":"DENY"
                        },
                        "cookies":{
?
                        },
                        "body":{
                            "code":"4001",
                            "success":false,
                            "msg":"供应商不存在"
                        }
                    }
                },
                {
                    "validate":{
                        "validate_extractor":[
                            {
                                "comparator":"not_equal",
                                "check":"body.msg",
                                "check_value":"供应商不存在",
                                "expect_value":"供应商添加成功",
                                "check_result":"pass"
                            }
                        ]
                    }
                }
            ]
        },
        {
            "details":[
                {
                    "results":{
                        "name":"查询员工数据",
                        "success":false,
                        "time":"2021-12-30 22:48:38",
                        "duration_ms":29.99,
                        "content_size":752
                    }
                },
                {
                    "request":{
                        "url":"http://47.101.111.187:8081/api/manage/search/supplier?page=1&size=10",
                        "headers":{
                            "User-Agent":"python-requests/2.26.0",
                            "Accept-Encoding":"gzip, deflate",
                            "Accept":"*/*",
                            "Connection":"keep-alive",
                            "Authorization":"JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImV4cCI6MTY0MTQ4MDUxOH0.nipBF4Ov1QgofPx7lGgoDPlS9_saWUbSpFUAzZfimts",
                            "Content-Length":"27",
                            "Content-Type":"application/json"
                        },
                        "method":"POST",
                        "body":{
                            "account":"",
                            "name":""
                        }
                    }
                },
                {
                    "response":{
                        "status_code":200,
                        "reason":"OK",
                        "headers":{
                            "Connection":"close",
                            "Content-Length":"752",
                            "Allow":"GET, POST, HEAD, OPTIONS",
                            "Content-Type":"application/json",
                            "Date":"Thu, 30 Dec 2021 14:48:38 GMT",
                            "Referrer-Policy":"same-origin",
                            "Server":"nginx/1.16.1",
                            "Vary":"Accept, Origin",
                            "X-Content-Type-Options":"nosniff",
                            "X-Frame-Options":"DENY"
                        },
                        "cookies":{
?
                        },
                        "body":{
                            "code":"2001",
                            "success":true,
                            "msg":"",
                            "total":4,
                            "data":[
                                {
                                    "id":58,
                                    "update_time":"2021-11-02 14:16:18",
                                    "create_time":"2021-11-02 14:16:18",
                                    "supplier_name":"1635833778109",
                                    "contacts":"解耦",
                                    "contacts_iphone":"",
                                    "remarks":""
                                },
                                {
                                    "id":57,
                                    "update_time":"2021-11-02 14:15:32",
                                    "create_time":"2021-11-02 14:15:32",
                                    "supplier_name":"1635833732898",
                                    "contacts":"解耦",
                                    "contacts_iphone":"",
                                    "remarks":""
                                },
                                {
                                    "id":56,
                                    "update_time":"2021-11-02 11:26:41",
                                    "create_time":"2021-11-02 11:26:41",
                                    "supplier_name":"1635823601082",
                                    "contacts":"解耦",
                                    "contacts_iphone":"",
                                    "remarks":""
                                },
                                {
                                    "id":42,
                                    "update_time":"2021-11-01 17:47:45",
                                    "create_time":"2021-11-01 17:47:45",
                                    "supplier_name":"供应商",
                                    "contacts":"李明明",
                                    "contacts_iphone":"15511112222",
                                    "remarks":"测试备注"
                                }
                            ]
                        }
                    }
                },
                {
                    "validate":{
                        "validate_extractor":[
                            {
                                "comparator":"equal",
                                "check":"body.code",
                                "check_value":"2001",
                                "expect_value":"2001",
                                "check_result":"pass"
                            },
                            {
                                "comparator":"equal",
                                "check":"status_code",
                                "check_value":200,
                                "expect_value":200,
                                "check_result":"pass"
                            },
                            {
                                "comparator":"length_greater_than",
                                "check":"body.data",
                                "check_value":[
                                    {
                                        "id":58,
                                        "update_time":"2021-11-02 14:16:18",
                                        "create_time":"2021-11-02 14:16:18",
                                        "supplier_name":"1635833778109",
                                        "contacts":"解耦",
                                        "contacts_iphone":"",
                                        "remarks":""
                                    },
                                    {
                                        "id":57,
                                        "update_time":"2021-11-02 14:15:32",
                                        "create_time":"2021-11-02 14:15:32",
                                        "supplier_name":"1635833732898",
                                        "contacts":"解耦",
                                        "contacts_iphone":"",
                                        "remarks":""
                                    },
                                    {
                                        "id":56,
                                        "update_time":"2021-11-02 11:26:41",
                                        "create_time":"2021-11-02 11:26:41",
                                        "supplier_name":"1635823601082",
                                        "contacts":"解耦",
                                        "contacts_iphone":"",
                                        "remarks":""
                                    },
                                    {
                                        "id":42,
                                        "update_time":"2021-11-01 17:47:45",
                                        "create_time":"2021-11-01 17:47:45",
                                        "supplier_name":"供应商",
                                        "contacts":"李明明",
                                        "contacts_iphone":"15511112222",
                                        "remarks":"测试备注"
                                    }
                                ],
                                "expect_value":8,
                                "check_result":"fail"
                            }
                        ]
                    }
                }
            ]
        },
        {
            "details":[
                {
                    "results":{
                        "name":"删除供应商",
                        "success":true,
                        "time":"2021-12-30 22:48:38",
                        "duration_ms":30.85,
                        "content_size":51
                    }
                },
                {
                    "request":{
                        "url":"http://47.101.111.187:8081/api/manage/supplier?id=58",
                        "headers":{
                            "User-Agent":"python-requests/2.26.0",
                            "Accept-Encoding":"gzip, deflate",
                            "Accept":"*/*",
                            "Connection":"keep-alive",
                            "Authorization":"JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImV4cCI6MTY0MTQ4MDUxOH0.nipBF4Ov1QgofPx7lGgoDPlS9_saWUbSpFUAzZfimts",
                            "Content-Length":"2",
                            "Content-Type":"application/json"
                        },
                        "method":"DELETE",
                        "body":{
?
                        }
                    }
                },
                {
                    "response":{
                        "status_code":200,
                        "reason":"OK",
                        "headers":{
                            "Connection":"close",
                            "Content-Length":"51",
                            "Allow":"GET, POST, PUT, DELETE, HEAD, OPTIONS",
                            "Content-Type":"application/json",
                            "Date":"Thu, 30 Dec 2021 14:48:38 GMT",
                            "Referrer-Policy":"same-origin",
                            "Server":"nginx/1.16.1",
                            "Vary":"Accept, Origin",
                            "X-Content-Type-Options":"nosniff",
                            "X-Frame-Options":"DENY"
                        },
                        "cookies":{
?
                        },
                        "body":{
                            "code":"2002",
                            "success":true,
                            "msg":"删除成功"
                        }
                    }
                },
                {
                    "validate":{
                        "validate_extractor":[
                            {
                                "comparator":"equal",
                                "check":"body.msg",
                                "check_value":"删除成功",
                                "expect_value":"删除成功",
                                "check_result":"pass"
                            }
                        ]
                    }
                }
            ]
        }
    ]
}
查看返回结果

也会有详细的运行日志,简单截了三张图

如果你们用过 httprunner2.x 以及 httprunner3.x 的话,你就会发现功能很相似。因为我读了 httprunner3 的源码之后自己写了一个,起名为 LRUN。很大的一部分代码都是从 httprunner3.x 拿过来 借鉴的。在此感谢 debugtalk 开源出这么优秀的框架。

那么我为什么要重新写一个呢?为什么不集成 httprunner3.x 呢? 

2020 年使用 Django +DRF 写的时候,我是集成了 Httprunner2.7 版本的,但目前作者已经不维护 2 版本了,最新的是 3 .x 的版本。

在我读了 httprunner3 大部分的代码之后,我发现 httprunner3.x 是不支持通过传入 yaml 或者 json 文件路径来运行的(读了源码没发现这样处理的逻辑,也可能是我没有找到,欢迎各位大佬进行指教)。这样的话集成平台就不太好集成(个人感觉),而且作者目前也是在开发 go 版本的,有 bug 也不能及时的修复。

最重要的一点是我自己写底层实现我可以很方便的进行扩展,例如支持 RPC、Mock 等。后期也可以自己实现。出了bug 也方便进行定位、修复。

第二部分是使用 FastAPI 写的接口,主要是将数据保存到数据库,组装成 LRUN 所需要的 yaml 格式,在传给 LRUN 进行运行,最后拿到测试结果返回给前端。

第三部分是 vue 写的前端页面,可以在页面上输入数据,调用接口保存的数据库中。

画了简易的架构图,如下

下面来看一下平台页面是怎样的

登录注册

登录页面

注册页面,和登录页面是一样的,只是一个 css 样式的翻转

登录成功之后进入到首页,主要是做一些数据的展示(近 6 个月新增 API 数接口还未实现)

左侧可以收起菜单,也可以全屏。最右边可以退出登录、修改密码、修改头像。

项目管理

可以进行项目的增删改查,也可以创建私有项目。

平台项目之间是进行隔离的,当你选择了项目之后,才能进行下面的操作。

点击项目名称进入到环境管理页面

环境管理

点了项目名称之后就进入到了环境管理页面,显示的是该项目下的所有环境

可以添加测试环境、开发环境、线上环境等等。

接口模板

接下来就是接口模板了,先来看看长啥样。是不是还挺漂亮哇~~

可以添加分组,将一个模块的接口放在一个分组下

点击分组下的接口,数据会渲染到右边,点击发送,就可以发送请求了,然后将响应结果渲染到页面上。

响应内容,是不是和 postman 页面类似。会显示 status code、接口运行时间、接口返回的数据大小。

灰色的一长串 uuid 为当前用例的 case_id,可以根据该 case_id 查询详细的信息

响应 headers

响应 cookies,因为我的接口没有返回 cookies,所以为空

断言页面,支持丰富的断言(往后看),满足你断言的各种方式。断言成功之后上面就会显示断言成功的绿色浮层,如果有一个断言失败的话,会显示红色的浮层

点击【添加接口】可以将右边所有的数据清空,就可以添加接口了。不清空的话,默认为编辑 api。

如何添加一个接口?

选择 SSL 验证、请求方式、环境、路径、是否跳过、循环次数(接口模板里的循环次数只在场景用例里生效,在接口模板里只会执行一次),如果有动态参数的话,可以通过 ${variable_name} 方式动态传参。

添加请求 headers

添加请求 body,这里集成了 json 编辑器,目前只支持 json 数据,右边两个按钮可以对参数进行格式化和压缩

断言,支持十多种断言方式。类型支持字符串、整形、布尔值

支持四种断言方式

  • 断言状态码:status_code

  • 断言响应头:headers.xxx

  • 断言cookies:cookies.xxx

  • 断言响应体:body.xxx

提取参数变量,支持从响应头、响应cookies、响应体里提取数据。其他地方可以通过 ${variable_name} 方式引用提取到的值

添加 hooks,可以在请求前和请求后做一些处理,支持添加多个 hooks。一般是在请求前对请求数据做一些加密处理和响应之后根据响应数据做些操作。

添加完成之后就可以进行保存了,可以选择要保存到的分组下

也可以进行删除

保存成功之后点击左边的接口,数据渲染到右边之后,就会出现删除按钮,这里我做了一个轻量的删除

上面基本上就是接口模板里的功能了,功能还是比较强大的,页面也是比较美观的。

测试用例

上面添加完了接口之后,就可以组装场景用例了。

场景用例一般是对一个场景进行测试,比如我登录成功之后,我去添加员工,添加完成之后我在查询数据,最后在删除数据。这些就可以看做是一个场景。

测试用例页面分为三个部分,最左侧的为组装好的测试用例,中间的为在接口模板里添加的接口,右边为场景用例列表。点击左侧的用例名称就会渲染在右边的测试用例列表页面,支持编辑更新。 

可以拖拽 API 列表里的接口到测试用例列表里,执行的时候会根据测试用例列表里的接口,从上往下一个一个执行,如果某个接口里设置的 times 是多次,则会将这个接口运行多次,运行完成之后在执行下一个接口。

可以自行排序,上下拖拽也可以进行排序,可以拖拽多个相同的 API 组成用例

鼠标放到 api 上会出现删除功能

也可以在测试用例页面进行 api 的编辑,这样在接口模板里只需要添加一个接口作为模板就可以了。例如一个登录接口,你要测正常的 case、还要测用户名密码分别错误的 case、为空的 case 等等。这时候你就可以将登录接口拖四五个到右边,然后在编辑接口,修改参数以及断言数据。

我们公司的接口,经常是传的参数不同,返回不同的数据。点击编辑进入到编辑页面,可以调试,也可以返回,也可以修改后保存。保存成功之后,用例列表会显示修改后的。

编辑页面,也支持调试接口,和接口模板里的功能一样

组装好用例之后就可以保存用例了,保存用例的时候可以选择发送邮件的方式。有以下三种方式,会发送到飞书群里,这里我飞书的地址在后端写死了,不支持修改,一般来说设置好飞书机器人之后就不会在变的。所以我这里不支持前端新增和修改。

保存之后就可以运行用例了,点击运行之后会在后台运行,因为当接口多的时候,比较耗费时间,当运行完成之后就可以查看测试报告了

飞书收到的测试结果如下,点击查看报告可以查看报告详情

测试报告

运行完场景用例之后,测试报告必不可少。可以查看总耗时、总用例数、成功数、失败数、跳过个数以及测试结果

也可以点击查看详情,这里参考了一个大佬写的报告样式。感谢大佬

也可以查看具体的请求数据、响应数据以及断言

定时任务

没有定时任务的测试平台是没有灵魂的,我们可以定期执行用例,不需要手动点击运行了。 

支持三种定时方式,一种是到某个时间执行一次,一种是每隔多久执行一次,最后一种就是 cron 方式了

某一刻执行一次任务,选择要执行的时间,右边选择执行的用例。

每隔多久执行一次,这里就可以设置多久一次,比如每天早上 10 点执行一次主要场景的接口,然后发送报告到群里,开发看到有失败的接口就可以去修复了。

也可以每隔 10 分钟跑一次,这样当线上出现故障的时候,我们就能及时的发现,而不是等用户反馈给我们。

cron 方式,传的是一个字典类型的

创建成功之后就可以启动任务了,默认创建完成之后任务是停止的,需要手动点击【启动】,任务才会启动,你也可以随时选择停止任务

定时任务执行成功之后,也会生成测试报告,可以进入到测试报告查看详情,也会发送飞书消息到群里

代码管理

代码管理的话主要是处理一些动态参数、数据的加解密、前置、后置做一些操作,可以在代码管理页面写 python 代码,也支持在线调试,写完之后可以通过 ${func_name()} 的方式来引用 

mock

功能还未实现,在开发中。。。

以上就是平台的主要页面和功能了。后期也会进行更新迭代的