<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 4827字,需瀏覽 10分鐘

           ·

          2022-03-16 09:21

          來源:云原生技術愛好者社區(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模擬代碼。
          • 確保您的測試可能會失敗。
          • 將不確定性排除在測試之外。
          這將使您的系統(tǒng)更加穩(wěn)定,另外經過良好測試的軟件讓您可以自信地進行更改和快速部署。
          引用
          • 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點 ,【冬哥有話說】, 邀請到劉曉玲老師分享“如何用測試搞垮軟件質量”。


          想要搞垮嘛?有意識無能力的速來,無意識有能力的也印證一下自己的做法


          立即報名,鎖定直播間!



          瀏覽 66
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久三级电影片 | 最新中文字幕在线免费观看 | 一区二区三区四区五区六区久久 | 91精品国产福利 | 欠欠欠久久精品一级探花 |