街口支付 API 自動化測試解決方案

「左思右想,街口還是需要自動化測試來承擔守護品質的責任」

JKOPAY SDET
街口支付 JKOPay
16 min readJan 3, 2022

--

Author

- Dopiz Liu
主要負責支付及應用領域,認為「身為一位測試開發人員的工作職責不僅僅是完成測試項目,而是無所不用其極地保證產品的最高品質」。

隨著軟體技術快速發展、系統複雜度不斷地提升,且導入敏捷開發後也讓產品迭代的速度越來越快,這就導致了測試人員在每次產品發布前執行迴歸測試的成本也越來越高,要維持軟體品質這件事也就變得更有挑戰性。

手動進行迴歸測試非常耗時,每次的版本發布測試人員除了要驗證新的功能外,還必須重複地執行相同且大量的迴歸測試案例,以確保這次的改動並未對既有功能造成任何負面影響。而當產品越來越複雜,測試案例就只會越來越多,相對應也就會花費更多的時間、更多的測試資源投入維護。因此在產品及業務相對穩定後,無論是透過 UI 來執行迴歸測試及相容性驗證,或透過 API 來執行單元測試、整合測試或效能測試,導入自動化流程都勢在必行。

自動化測試的好處相信大家都知道,可重用的程式碼讓測試開發人員只需要撰寫一次就能夠在往後測試時多次重複使用,不僅節省時間成本,也讓測試人員能夠提高對專案掌控的維度,在不同的環節中投入資源。

  • 測試左移:測試人員在開發自動化測試案例時,會需要統整規格及開發文件來設計測試案例及情境,因此能夠儘早發現規格文件上的錯誤或是能夠優化的方向,並提交給 PM & RD。
  • 冒煙測試:當開發人員在測試環境部署後,自動化測試可以在手動測試前試探環境或是確認最基本的測試案例是否通過。
  • 監控服務:定期執行自動化測試可以及早發現問題,增強對服務異常的感知能力。關於街口的監控服務簡單實踐,可以參考這篇文章分享。

即便如此,仍有許多類型的測試案例是無法透過自動化測試來執行的,因此手動的探索性測試依舊是非常重要的測試環節。

這篇文章的內容主要會分享街口測試團隊在 API 自動化測試 所使用的工具及選用的框架應用。而自動化框架的選型有興趣的也可以先參考之前的文章 — 街口支付軟體測試自動化方案選型

在講架構之前,想先提一下著名的康威定律

Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization’s communication structure.

— Melvin E. Conway, 1967

我們認為不僅是開發人員的程式設計架構會根據組織結構反映在系統中,測試人員在開發測試框架時同樣也會受到組織的各項策略影響,包含業務需求、技術能力、組織結構及測試策略。不同領域的面向也都會為每間公司帶來不同的方向,也需要依據這些差異來設計最符合需求的測試方法。

▲ Factors of Testing Architecture

在我們自動化測試的程式架構中主要分為幾個部分:

  • Tests — 測試案例執行的 Script
  • API — 各 Service 的 API 請求介面
  • Test Data — 以特定格式儲存的測試資料
  • Utility — 封裝各種共用方法以提供調用
  • Report — 產生測試結果的流程及後續應用
▲ JKOPay API Automation Testing Architecture

Tests

在測試過程中執行的程式碼除了實際執行測試案例與驗證外,也包含相關的 configurations 及 common method 等共用的 function,以下會描述資料夾分層結構及各個區塊的作用:

.
├── conftest.py
├── configurations
│ ├── default.py
│ ├── test.py
│ └── ...
├── common
│ ├── database
│ │ ├── base_database.py
│ │ ├── service_A_database.py
│ │ ├── service_B_database.py
│ │ └── ...
│ ├── payment_module.py
│ ├── user_module.py
│ └── ...
└── tests
├── service-A
│ ├── component-A
│ │ └── test_component_A.py
│ ├── component-B
│ └── ...
├── service-B
├── service-C
└── ...

✔ ️conftest.py

首先 conftest 作為 pytest 的入口點也可以作為全局的 setup & teardown。而在我們的架構中,測試執行前會做幾件必要執行的步驟:

  • 增加 command option,包含指定要跑的 target_case_ids 或是指定的 target_marks,以及要執行測試的環境env

target_case_ids 的功能可以讓 Script 只執行指定 case_id 的測試案例。

target_marks 的用法則是類似 pytest 原先 mark decorator 的使用機制,為了讓其更加彈性,我們選擇將各個測試案例的 mark 放在測試資料中而非寫死在 Script 當中,如此一來就可以將特定業務需求的測試案例打包,讓該 Test Run 只執行指定 marks 的項目。

env 我們希望同一份程式碼能夠在各個環境執行測試,只需要傳入不同的環境參數後,就可以調用不同的 config 來執行測試以達到重用程式碼的目的。

  • 選擇欲執行測試的 config 檔案

會使用這個方式是受到 ansible 機制的啟發,在 configurations 中我們會預先寫好各個環境的 config 內容,進入測試之前會複製一份相對應環境的 config 檔案作為 env.py ,而程式碼中每次執行測試都只會 import 使用這個檔案裡面的參數內容,以達到針對不同環境進行測試的目的。

▲ Pytest Configure Script Example
  • 處理執行參數的前置作業

舉例來說像是全局參數的設置、特定功能的開關等操作,會在此階段根據不同的測試策略進行處理。

最後在 conftest 中也會自定義一些通用的 fixture 來使用,舉例來說我們設計了一個 is_run 的 function,在每個測試案例執行前都會使用到它來判斷這個 Test Case 是否有需要被執行或跳過;或是會將登入相關的功能打包為一個 setup fixture 來進行使用。

▲ Fixture Example

✔ ️Configurations

Tests 中的 config 檔案主要會包含各個 Service 的資料庫 connection 時用的參數,或是 Redis、Kafka 的連線參數等。

✔ ️Common

在不同的 component 中仍然會有一些可以共用的參數或功能。

舉例來說,我們會封裝連接各個資料庫的方式打包成 class,再開始撰寫 SQL 或是透過 ORM 的方式取得資料表內容,加以驗證測試案例的資料流或是設定測資的前置作業。但為了確保能夠跨 componenet 共用,因此在設計 query 時需要保持彈性、盡量抽離業務邏輯以達到此目的。

另一個例子則是模擬用戶的 class,同樣封裝用戶登入時的幾支 API、設置好幾個常用的 property,讓我們在寫測試案例時只要將手機號碼傳入,就可以透過該物件取得需要的用戶相關資訊。

✔ ️Test Script

之前分享的文章中提到的相似,測試執行的流程是先將 Test Data 讀取進來,再透過 pytest.mark.parametrize 的 decorator 將各條測試案例的測資傳入 Test function 中。

在測試同一隻 API 的過程中,可能同時會有正向案例及負向案例,在能夠重用程式碼、方便維護的前提下,會希望使用同一個 function 就可以執行多種測試情境。像是範例的程式碼中會有一些判斷式用來決定某些參數的來源是由真實資料流引入或是假資料,分別模擬正負向案例。其餘就是業務邏輯的驗證,無論是對 response 的欄位或是對資料庫中記錄的內容進行確認,都是我們需要校驗的部分。

▲ Test Case Script Example

API

在 API 的部分我們將其存在另一個 repository,並且作為 Tests 的 submodule 來使用。會這樣做的原因是因為我們希望 API 能夠獨立運作且跨專案來被調用。舉例來說除了 API 的 Unit Test 外,還有 Integration (Scenario) Test 會用到同一份 API 的程式碼,甚至 UI 的自動化測試有時也會使用部分 API 來輔助測試案例的執行。若每一個專案都要撰寫同樣的程式碼就會降低可重用性及可維護性。

而在資料夾結構中,切分的粒度會先以 Service 劃分,再新增各個 Service 底下不同的 Component API 來使用,實際結構大概如下:

.api (submodule)
├── base_api.py
├── configurations
│ ├── default.py
│ ├── test.py
│ └── ...
├── service-A
│ ├── component_A_api.py
│ ├── component_B_api.py
│ ├── constants.py
│ ├── encode.py
│ └── ...
├── service-B
└── ...

✔ ️Configurations

因為上述提到我們希望 API 要能夠獨立使用,所以相關的 config 參數就必須放在這個 repository 中。內容包含各個 Service 的 domain url、或是預設的 timeout 時間等多項參數。

✔ ️Base API

所有的 Component API 都會繼承 BaseAPI 類別,用以執行 API 請求、timeout 的限制處理以及 Allure Report 的設定,如此一來就不會散落在各個 API function 中。另外在 timeout 的參數設計上,我們希望全局有一個預設的閾值,但各個 Service 也可以因應服務的複雜度來客製自己的閾值;甚至在特定的 API 有可能與第三方串接而導致響應速度較慢,因此也將 timeout 閾值的粒度下放到每支 API 皆可單獨設置的層級。

▲ BaseAPI Class

✔ ️Component API

在 API Class 中,會先定義好該 Component 的 base_url,再將其底下的 API Path 都先宣告成 class variable 以便能夠一眼就看出有哪些 API。

API function 傳參的部分則會根據參數的數量、能否有效重複利用,來決定需不需要將參數組合成 dataclass 來做使用。組合好對應的 request body 後,就透過繼承的父類別 method 來進行請求,並回傳 response object。

▲ ComponentAPI Class

✔ ️Encode & Constants

encode.py 主要會存放該 Service 用來執行 encode 或是 hash 所使用到的演算法,舉例來說有些 API 必須針對 request body 進行加/解密才能夠進行請求/解析回應,以利後續進行驗證。

️constants.py 則會預先定義好 Service 會用到的 enum、map 或是 dataclass。舉例來說支付工具的類型在資料庫或是 response 使用時可能是以 int 的型態做儲存,而我們會需要實際 mapping 哪一個數字是對應何種支付工具。

Test Data

測試資料的部分我們也將其作為另一個 repository 儲存,但不作為 submodule。因為測試資料是變動頻率最高的部分,無論是修正測資、需要暫時 Skip Case,都會需要更新檔案後重新 push,若跟 Test Script 放在一起的話,commit 紀錄就會變得非常雜亂。

在結構方面,我們會希望所有環境的測資都放在同一個 repository,只需要在執行測試前選擇不同的 config 就可以對應不同的環境及測資來執行測試。但每個環境的測資內容又不盡相同,因此 test data 會比其他部分多拆分環境 (env) 這一層。

.testdata
├── env-TEST
│ ├── service-A
│ │ ├── component_A.xlsx
│ │ └── component_B.xlsx
│ ├── service-B
│ │ └── component_C.xlsx
│ └── ...
├── env-PROD
│ └── ...
└── ...

我們主要以 Excel 檔案來存放測資,並透過 pandas 套件來存取其內容,再將各個 Sheet 的每一行內容作為 dictionary 餵進 Test Script 來逐項執行測試案例。而在檔案內容中主要會記錄幾項測資的內容:

  • case_id:用以記錄該 case 的唯一識別碼,可以透過這個 id 來指定執行該測試案例
  • marks:該 case 被標上的 mark 可能有多個,而測試進行時可以指定執行哪些 marks 的測試案例
  • is_run:標註該 case 的狀態,是否需要被執行或是 Skip
  • Request Parameter:剩下最重要的部分則是 Script 要執行時的必要參數、API request header/body/params 以及預期要進行驗證的相關欄位等
▲ Test Data Example

Utility

Utility 中包含一些工具類的 function 提供調用,像是加上 error handling 的型態轉換 method、封裝並擴充 datetime/dateutil library 等時間相關功能、各資料庫連接方式以及檔案處理相關等功能。而同樣地,Utility 也是透過 submodule 的機制來提供給別的專案使用。

Report

▲ API Automation Testing Flow

在自動化腳本開發到一個程度後,我們會利用 Jenkins 串接起上下游專案,在 Service 部署成功後會主動觸發自動化測試,透過測試執行結果來確認新版本的可用性及穩定性。

首先上游的 Jenkins Job 在部署成功後,會先觸發針對測試環境的 Health Check,我們需要透過一些基本的測試案例如登入、支付、轉帳來確保目前測試環境正常。確認環境正常後,Health Check 會根據上游的來源決定要執行相對應的自動化測試 Job。自動化測試執行時,我們也有設計一個自動 re-run 的機制,避免因為環境不穩定或是任何原因造成的 flaky tests。

測試報告產出是使用了 Allure Report 的套件來做呈現,此外我們也有針對 Allure 產出的 JSON 檔案進行一些處理讓 parameters 以及 request body 的呈現更直覺。在測試報告中可以看到各個測試案例執行的狀態及結果,包含執行花費的時間、測資中涵蓋的欄位、實際對 API 進行請求的參數等。如此一來,在測試失敗時開發人員就可以直接使用相同的測資來嘗試復現問題。

▲ Allure Report Example

最後,測試結果的通知直接使用了 Jenkins 的 Slack Extension 來串接,訊息中會將測試報告的連結附在訊息之上,並且及時通知相關的負責人員以便能夠儘速修復,維持服務穩定性。

▲ Slack Notification

Test Integration Platform

在每次測試執行後,我們都會將結果記錄到資料庫中,並在每週固定產出對應的報告呈現給技術部成員們了解測試環境的穩定度。初期我們設定排程執行簡單的 Script 來產出圖表,附在週報或是 Slack 中分享。隨著開發的輔助工具越來越多,我們決定建立一個測試整合平台,將各種 Script 以及小工具都彙整到平台之中,提供開發團隊完整且便利的測試方案。

在我們目前的平台中,有像是 Stability View 的功能,能夠讓開發人員在部署完 Service 後,立即到平台中查看是否有相對應的自動化測試被執行、執行結果是否如預期,當然要查歷史紀錄也沒問題!

還有 Review Hub 搜集了在 Google Play & App Store 對街口 App 的評論,以及在 PTT 的 MobilePay、Lifeismoney、Gossiping 等看板上大家對街口的評論,並且自動幫每則 review 標註上相關的 tag 以便之後的回溯與分析,透過這些反饋持續地優化使用者體驗。

另外 Handy Tools 中也整合了像是建立各種測試帳號、調整用戶餘額等非常多項小工具,不僅能夠加快測資建立的速度,同時提升開發人員在開發及修復上的效率,連不了解業務邏輯的非技術人員也都能輕易上手、使用!

目前也還有許多需求等著被實現,之後有機會也會分享我們實作這個平台的想法給大家。

▲ JKOPay Test Integration Platform
▲ JKOPay SDET Team Roadmap

回顧在初期高速發展的街口,不穩定的品質不僅困擾著用戶的使用體驗,也讓開發人員的產出沒有穩定的迭代,更讓每次的產品發布如履薄冰。而測試團隊加入後,快速地將既有功能及服務透過手動測試、探索性測試確保整體品質穩定提升,並將線上問題復現後逐一修復。直到我們開始推進自動化測試的開發,不斷的增加測試案例、優化自動化測試流程及架構後,提升了每次產品發布的穩定性,也降低了線上問題的發生。

而隨著街口業務的持續發展,自動化測試案例理所當然會不斷增加,但如何將測試案例去蕪存菁的同時,又能維持同樣的高測試覆蓋率;或是在測試案例之間導入 dependency 的概念增加執行效率,甚至實踐平行化的自動化測試以加快測試執行時間等等,這些都是我們要持續努力的目標。

最後還是推廣一下,無論是自動化框架的優化、又或是測試平台的擴展,都還需要更多對測試有興趣的夥伴們。歡迎投遞履歷,跟我們一起實踐並持續完善街口的測試目標😀。

JKOPay SDET Team
2022.01.03

--

--