為什么要寫測試平台
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" } ] } } ] } ] }
也會有詳細的運行日志,簡單截了三張圖



如果你們用過 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
功能還未實現,在開發中。。。
以上就是平台的主要頁面和功能了。后期也會進行更新迭代的
