測試工作中常用到的測試樁mock能力
在我們的測試工作過程中,可能會(huì)遇到前端服務(wù)開發(fā)完成,依賴服務(wù)還在開發(fā)中;或者我們需要壓測某個(gè)服務(wù),而這個(gè)服務(wù)的依賴組件(如測試環(huán)境MQ
) 無法支撐并發(fā)訪問的場景。這個(gè)時(shí)候我們可能就需要一個(gè)服務(wù),來替代測試環(huán)境的這些依賴組件或服務(wù),而這就是本文的主角--測試樁。
測試樁可以理解為一個(gè)代理,它可以用于模擬應(yīng)用程序中的外部依賴項(xiàng),如數(shù)據(jù)庫、網(wǎng)絡(luò)服務(wù)或其他API,它可以幫助我們在開發(fā)和測試過程中隔離應(yīng)用程序的不同部分,從而使測試更加可靠和可重復(fù)。
應(yīng)用場景
測試樁使用的一般有以下幾種場景:
本文將選取常用的幾個(gè)場景循序漸進(jìn)地介紹測試樁的開發(fā)和優(yōu)化。
簡單測試樁
如果在測試環(huán)境中不方便安裝其他的庫,我們可以使用Python標(biāo)準(zhǔn)庫中的一個(gè)模塊http.server
模塊創(chuàng)建一個(gè)簡單的HTTP請求測試樁。
# simple_stub.py
# 測試樁接收GET請求并返回JSON數(shù)據(jù)。
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
content = json.dumps({"message": "Hello, this is a test stub!"}).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", f"{len(content)}")
self.end_headers()
self.wfile.write(content)
if __name__ == "__main__":
server_address = ("", 8000)
httpd = HTTPServer(server_address, SimpleHTTPRequestHandler)
print("Test stub is running on port 8000")
httpd.serve_forever()
運(yùn)行上面的代碼,將看到測試樁正在監(jiān)聽8000端口。您可以使用瀏覽器或curl
命令訪問 http://localhost:8000
,將會(huì)收到 {'message': 'Hello, this is a test stub!'}
的響應(yīng)。
http.server
擴(kuò)展:一行命令實(shí)現(xiàn)一個(gè)靜態(tài)文件服務(wù)器
http.server
模塊可以作為一個(gè)簡單的靜態(tài)文件服務(wù)器,用于在本地開發(fā)和測試靜態(tài)網(wǎng)站。要啟動(dòng)靜態(tài)文件服務(wù)器,請?jiān)诿钚兄羞\(yùn)行以下命令:
python3 -m http.server [port]
其中[port]是可選的端口號,不傳遞時(shí)默認(rèn)為8000。服務(wù)器將在當(dāng)前目錄中提供靜態(tài)文件。
如在日志文件夾中執(zhí)行python -m http.server
,就能在web瀏覽器中訪問這個(gè)文件夾中的文件和子文件夾的內(nèi)容:
注意: http.server
主要用于開發(fā)和測試,性能和安全方面不具備在生產(chǎn)環(huán)境部署的條件
性能優(yōu)化:使用異步響應(yīng)
我們在前面的實(shí)現(xiàn)了一個(gè)簡單的測試樁,但在實(shí)際應(yīng)用中,我們可能需要更高的性能和更復(fù)雜的功能。
異步響應(yīng)
在只有同樣的資源的情況下,像這樣的有網(wǎng)絡(luò)I/O的服務(wù),使用異步的方式無疑能更有效地利用系統(tǒng)資源。
說到異步的http框架,目前最火熱的當(dāng)然是FastAPI
,使用FastAPI
實(shí)現(xiàn)上面的功能只需兩步。
首先,安裝FastAPI和Uvicorn:
pip install fastapi uvicorn
接下來,創(chuàng)建一個(gè)名為fastapi_stub.py
的文件,其中包含以下內(nèi)容:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def get_request():
return {"message": "Hello, this is an optimized test stub!"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
執(zhí)行代碼,這個(gè)測試樁也是監(jiān)聽在8000端口。我們可以像之前那樣使用瀏覽器或其他HTTP客戶端向測試樁發(fā)起請求。
異步編程優(yōu)勢 介紹
需要注意的是,雖然異步編程在許多場景下可以提供更好的性能,但它并不總是比同步編程快。在某些情況下,如CPU密集型任務(wù),異步編程可能無法帶來明顯的性能提升。此外,異步編程通常需要更復(fù)雜的編程模型和錯(cuò)誤處理機(jī)制,因此在選擇異步編程時(shí)需要權(quán)衡其優(yōu)缺點(diǎn)。
性能優(yōu)化:利用多核
雖然我們前面使用到了異步的方式來提升測試樁的性能,但是代碼還是只是跑在一個(gè)CPU核心上,如果我們要進(jìn)行性能壓測,可能無法滿足我們的性能需求。這個(gè)時(shí)候我們可以使用 gunicorn
庫 來利用上服務(wù)器的多核優(yōu)勢。
gunicorn
Gunicorn的主要特點(diǎn)和優(yōu)勢:
安裝 gunicorn
pip install gunicorn
使用 gunicorn 啟動(dòng)服務(wù)
啟動(dòng)服務(wù):
gunicorn -w 4 fastapi_stub:app
可以看到,上面的命令啟動(dòng)了4個(gè)worker 進(jìn)程,大家也可以使用ps -ef
命令查詢一下進(jìn)程狀態(tài)。
gunicorn的一些常用參數(shù):
Gunicorn提供了許多其他配置選項(xiàng),可以根據(jù)具體需求進(jìn)行調(diào)整。要查看完整的選項(xiàng)列表,可以查看Gunicorn的官方文檔:https://docs.gunicorn.org/en/stable/settings.html。
性能優(yōu)化:使用緩存(functools.lru_cache
)。
當(dāng)處理重復(fù)的計(jì)算或數(shù)據(jù)檢索任務(wù)時(shí)。使用內(nèi)存緩存(如Python的functools.lru_cache)或外部緩存(如Redis)來緩存經(jīng)常使用的數(shù)據(jù)也能極大的提升測試樁的效率。
假設(shè)我們的測試樁需要使用到計(jì)算計(jì)算斐波那契數(shù)列這樣耗時(shí)的功能,那么緩存結(jié)果,在下次遇到同樣的請求時(shí)直接返回而不是先計(jì)算再返回,將極大的提高資源的使用率、減少響應(yīng)的等待時(shí)間。
如果僅僅是直接返回?cái)?shù)據(jù)的,沒有進(jìn)行復(fù)雜的計(jì)算的測試樁,使用
lru_cache
并沒有實(shí)際意義。
以下是一個(gè)更合適的使用lru_cache
的示例,其中我們將對斐波那契數(shù)列進(jìn)行計(jì)算并緩存結(jié)果:
from fastapi import FastAPI
from functools import lru_cache
app = FastAPI()
@lru_cache(maxsize=100)
def fibonacci(n: int):
if n <= 1:
return n
else:
return fibonacci(n - 1) + fibonacci(n - 2)
@app.get("/fibonacci/{n}")
async def get_fibonacci(n: int):
result = fibonacci(n)
return {"result": result}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
在這個(gè)示例中,我們使用FastAPI創(chuàng)建了一個(gè)簡單的HTTP請求測試樁。我們定義了一個(gè)名為fibonacci
的函數(shù),該函數(shù)計(jì)算斐波那契數(shù)列。為了提高性能,我們使用functools.lru_cache
對該函數(shù)進(jìn)行了緩存。
在路由/fibonacci/{n}
中,我們調(diào)用fibonacci
函數(shù)并返回結(jié)果??梢悦钤L問 http://localhost:8000/fibonacci/{n}
進(jìn)行調(diào)試。
需要注意的是 maxsize
參數(shù)是functools.lru_cache
裝飾器的一個(gè)配置選項(xiàng),它表示緩存的最大容量。lru_cache
使用字典來存儲(chǔ)緩存項(xiàng),當(dāng)一個(gè)新的結(jié)果需要被緩存時(shí),它會(huì)檢查當(dāng)前緩存的大小。如果緩存已滿(即達(dá)到maxsize
),則會(huì)根據(jù)LRU策略移除最近最少使用的緩存項(xiàng)。如果maxsize
設(shè)置為None
,則緩存可以無限制地增長,這可能導(dǎo)致內(nèi)存問題。
單元測試中的mock
Python unittest.mock
在Python中,unittest模塊提供了一個(gè)名為unittest.mock的子模塊,用于創(chuàng)建mock對象。unittest.mock包含一個(gè)名為Mock的類以及一個(gè)名為patch的上下文管理器/裝飾器,可以用于替換被測試代碼中的依賴項(xiàng)。
import requests
from unittest import TestCase
from unittest.mock import patch
# 定義一個(gè)函數(shù) get_user_name,它使用 requests.get 發(fā)起 HTTP 請求以獲取用戶名稱
def get_user_name(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()["name"]
# 創(chuàng)建一個(gè)名為 TestGetUserName 的測試類,它繼承自 unittest.TestCase
class TestGetUserName(TestCase):
# 使用 unittest.mock.patch 裝飾器替換 requests.get 函數(shù)
@patch("requests.get")
# 定義一個(gè)名為 test_get_user_name 的測試方法,它接受一個(gè)名為 mock_get 的參數(shù)
def test_get_user_name(self, mock_get):
# 配置 mock_get 的返回值,使其在調(diào)用 json 方法時(shí)返回一個(gè)包含 "name": "Alice" 的字典
mock_get.return_value.json.return_value = {"name": "Alice"}
# 調(diào)用 get_user_name 函數(shù),并傳入 user_id 參數(shù)
user_name = get_user_name(1)
# 使用 unittest.TestCase 的 assertEqual 方法檢查 get_user_name 的返回值是否等于 "Alice"
self.assertEqual(user_name, "Alice")
# 使用 unittest.mock.Mock 的 assert_called_with 方法檢查 mock_get 是否被正確調(diào)用
mock_get.assert_called_with("https://api.example.com/users/1")
總結(jié)
在開發(fā)測試樁時(shí),我們需要根據(jù)實(shí)際需求和后端服務(wù)的特點(diǎn)來設(shè)計(jì)測試樁的行為,為的是使其更接近實(shí)際后端服務(wù)的行為,確保測試結(jié)果具有更高的可靠性和準(zhǔn)確性。
可能還有其他的優(yōu)化方案,歡迎大家提出。希望本文能對大家的工作帶來幫助。