單元測試最佳實踐:如何避免常見陷阱?| IDCF

來源:云原生技術愛好者社區(qū) 作者:半夏透心涼?
單元測試的目的是為了隨著時間的變化,系統(tǒng)能夠按預期工作。一來系統(tǒng)質量得到了保證,開發(fā)人員能夠提前發(fā)現和解決問題,不用身陷bug的泥潭無法自拔;二來開發(fā)人員有更多的時間和精力去完善自己技術、提升自己的生活質量,從而形成一個良性循環(huán)。

我寫了很多測試,也讀了很多。他們中的大多數幫助我及早發(fā)現錯誤,提供代碼文檔并幫助回歸測試。但我也發(fā)現一些單元測試沒有做到這一點。相反,它們要么非常復雜,以至于無法弄清楚它們在測試什么,要么會隨機失敗,要么根本不會失敗。
本文介紹了導致單元測試無效的五個陷阱,以及如何修復它們。
一、為每個函數編寫一個單元測試
看起來很簡單。假設您有一個小函數可以做一件事。假設它被稱為calculate_average。它是一個小單元,它是單元測試最佳實踐希望您測試的單元。所以你為它寫了一個測試,test_calculate_average.
這有什么問題?它測試單個代碼單元,但它應該測試該單元的單個行為。通常這也被表述為在測試中只有一個斷言。一個更好的測試將是test_calculate_average_return_0_for_empty_list. 一旦您擁有了其中的幾個,他們就會免費為您提供詳細的文檔。
它還改變了您對如何編寫測試的思維方式。您必須考慮您期望從函數中獲得的不同行為。在不知不覺中,場景越來越多,因為您正在考慮邊緣情況,甚至為它們編寫測試,所以編寫單元測試的收益也逐漸降低。
為每個功能單元編寫一個單元測試,而不是代碼單元。
測試的重點應該是外部行為,如果我們過渡關注內部行為,當我們對實現邏輯進行了修改,那么原本的單元測試也就無法使用了,也起不到對代碼重構保駕護航的作用了,違背了我們寫單元測試的初衷,當然如果有一塊內部邏輯,非常復雜,你也可以自己進行全覆蓋測試,但一般情況下沒有必要為了測試而測試。
二、只為代碼覆蓋率而編寫測試
跟蹤測試覆蓋率通常是一個好主意。如今,許多測試框架都支持這一點,并且像codecov這樣的平臺可以很容易地隨著時間的推移對其進行跟蹤。那么,為什么沉迷于它不是一個好的想法呢?
代碼覆蓋率只是一種測量工具。100% 的代碼覆蓋率并不意味著你已經覆蓋了所有的邊緣情況,它只是意味著所有的代碼路徑都被執(zhí)行了。這是一個覆蓋率 100% 的快速反例,但讓我們探討當您傳入一個空列表時會發(fā)生什么?
def average(elements: List[int]):
return sum(elements) / len(elements)
def test_average_returns_average_of_list:
result = average([1,3,5,7])
assert result == 4代碼覆蓋率的根本問題是它只衡量覆蓋了多少行程序。但所有程序都是狀態(tài)機;要獲得完整覆蓋,您必須覆蓋所有狀態(tài),但這是不可行的。
追求完整的,或者至少是非常高的覆蓋率也會導致大量的測試,但并不是所有的測試都那么有用。對于膠水代碼尤其如此。我見過模擬 Web 框架 (flask) 一半的測試,只是為了測試為端點注冊函數是否有效。這是測試一小部分功能的大量工作。如果你弄錯了,那就很明顯了。一旦你做對了,它在未來不太可能改變。
我沒有努力覆蓋每一行代碼,而是推薦 Martin Fowler 的建議。將測試重點放在有風險的代碼上。那是您自己編寫的代碼,而不是可能會被重構的框架。然而,知道什么是有風險的很困難,因為它需要經驗。
您應該將 [您的測試工作] 集中在風險點上。— Martin Fowler,重構
特別是某個代碼邏輯導致的線上bug,或者其它同學發(fā)現的問題,都可以編寫成測試用例,防止此類錯誤的再次出現。
三、嚴重依賴Mock
使用打樁模擬和存根對于單元測試是必不可少的。大多數情況下,您的被測代碼與其他模塊交互,并且在測試期間,您希望控制它們的行為。這可能導致你過度打樁。
當您必須編寫 50 或 100 行模擬來測試單個函數時,那么您在測試什么?您是在測試您的函數,還是在測試您為測試該函數而編寫的模擬?
許多Mock模擬也是危險信號。當您需要多個非常復雜的模擬來測試單個函數時,這個函數很可能復雜度過高。因此,您可能希望將其重構為幾個功能較少且可以單獨測試的函數。我見過一些非常復雜的模擬。這是一個例子的再現:
# custom_middleware.py ####################################
class CustomHeaderMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
response = await call_next(request)
response.headers["CustomField"] = "bla"
return response
# test_custom_middleware.py ###############################
async def endpoint_for_test(_):
return PlainTextResponse("Test")
middleware = [Middleware(CustomHeaderMiddleware)]
routes = [Route("/test", endpoint=endpoint_for_test)]
app = Starlette(routes=routes, middleware=middleware)
@pytest.mark.asyncio
async def test_middleware_sets_field():
client = TestClient(app)
response = client.get("/test")
assert response.headers["CustomField"] == "bla"這個時候,你不要想辦法進行Mock模擬,而是考慮如何進行重構?讓其變得更簡單,更容易測試。
我們通常通過單元測試去保證代碼質量,那么單元測試代碼本身的質量又如何保證呢?所以我們的單元測試要寫的盡可能簡單。
對于對數據一致性要求不高的系統(tǒng),甚至可以直接對著接口進行測試,這樣省去了編寫Mock的復雜度。
四、編寫永不失敗的單元測試
正常情況下,回歸是進行單元測試的原因之一。您編寫代碼,編寫通過的測試并獲得收益。萬一有人破壞了您代碼的功能,單元測試將能夠發(fā)現問題。然而,另外一種情況,您的測試可能永遠不會失敗并且您會錯過回歸。
但是,您如何以永不失敗的測試結束呢?下面是一個例子:
def get_film(id: str):
data = {"query": QUERY, "variables": json.dumps({"id": id})}
response = requests.post(URL, data=data)
return response.json()["data"]["film"]
def test_get_film_returns_successfully():
mock_response = {
"data": {
"film": {
"title": "a New Test",
"id": "testId",
"episodeID": 4
}
}
}
with requests_mock.Mocker() as mock:
mock.post(URL, json=mock_response)
result = get_film("foo")
assert result == {
"title": "a New Test",
"id": "testId",
"episodeID": 4
}現在問問自己:哪些更改會導致此測試失敗?最明顯的一個是改變Mock模擬響應。但這不算數,您沒有更改被測代碼。更糟糕的是,我忘記了傳遞json.dumps參數. 這個錯誤不會被測試發(fā)現。另外有的同學為了保證測試覆蓋率,甚至不寫斷言,直接打印輸出,這樣的話,可能永遠不會出錯。
這種問題被稱為誤報,看似無懈可擊的測試用例,其實沒什么用處,為了防止這種情況,請考慮是什么導致您的測試失敗。更好的是,從失敗的測試開始,然后編寫代碼直到它通過。在不知不覺中,您正在進行測試驅動開發(fā)。
五、使用單元測試保證非確定性行為的正確性
這是一個眾所周知的謬論。如果您的測試或被測代碼以不確定的方式運行,您將對測試失去信心。每次失敗時,你都會問:我的測試失敗了,還是會通過重新運行?重新修改運行都會給你的測試用例帶來修改的麻煩,你甚至想要放棄單元測試用例。
對于測試來說,不確定性的缺點是顯而易見的,那么是什么導致了這種情況呢?
您是否在測試中使用當前時間或日期?如果是,則您的測試每天都在使用不同的數據運行。一旦您從事該行業(yè)的時間足夠長,您就會遇到這些類型的測試。它們可能僅在該月的最后一天失敗,或者僅在午夜之前開始并在之后完成。幸運的是,有一個簡單的解決方案:控制時間的流動。例如,Python 具有用于此的freeze-gun模塊。
您是否使用隨機性來生成示例數據?有一個名為faker的 Python 庫,它可以輕松生成真實的數據,如姓名、地址或電話號碼。它非常適合填充演示環(huán)境或冒煙測試。對于單元測試不是那么有用,通常而言,使用硬編碼的單元測試用例最可靠。
如果系統(tǒng)中存在不確定性,那么應該保證固定的邏輯不會出錯,對于不確定性的邊緣情況應該通過其它方式保證,比如開發(fā)、測試人員、尋找更穩(wěn)定的類庫等。
總結
這就是阻止您編寫有效單元測試的五個陷阱。既然您了解它們,您可以通過執(zhí)行以下操作來避免它們:
為功能的每個部分而不是每個函數編寫測試。 不癡迷于代碼覆蓋率,而是專注于測試有風險的代碼。 最小化Mock模擬代碼。 確保您的測試可能會失敗。 將不確定性排除在測試之外。
https://github.com/google/googletest
https://betterprogramming.pub/advanced-unit-tests-5-pitfalls-and-how-to-avoid-them-eb6e04ec9654
https://developer.ibm.com/articles/au-googletestingframework/
https://www.froglogic.com/blog/code-coverage-of-unit-tests-written-with-google-test/

今晚8點 ,【冬哥有話說】, 邀請到劉曉玲老師分享“如何用測試搞垮軟件質量”。
想要搞垮嘛?有意識無能力的速來,無意識有能力的也印證一下自己的做法
立即報名,鎖定直播間!

