为什么要写测试平台
从事测试行业也有几年了,也用过很多的测试平台。之前看到过一位大佬说,当你从事测试几年后,你是想当一个工具的制造者还是做一个使用者。我想,我是想当一个制造者。
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 - 删除成功
各参数意义如下
- test:一个 test 为一个接口用例
-
times:接口运行次数
-
skip:是否跳过接口
-
SSL:是否开启 SSL 验证。
-
mock:是否 mock 接口(目前功能还未实现)
-
request:填写请求信息,请求方式、请求体、path 等
-
extract:提取接口响应里的参数,支持提取响应头、响应体、响应 cookies。
-
validate:断言,支持丰富的断言方式(13种断言方式,满足接口测试断言的多种场景),eq 表示断言相等,not_equal 不等于
-
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" } ] } } ] } ] }