## 使用 Pytest 進行單元測試(2) <br/> ### Max Lai --- ### 今天的 demo code https://github.com/cclai999/pytest-0706 --- ### [Test double] + How can we verify logic independently when code it depends on is unusable? + How can we avoid Slow Tests? source: [xUnit Patterns - Test Double](http://xunitpatterns.com/Test%20Double.html) ---- Apollo 13 - Flight Simulator {%youtube JgTfISjq4o4 %} <div style="font-size: 18px; text-align: left;"> <a href="https://www.youtube.com/watch?v=JgTfISjq4o4">Apollo 13 Movie Scene Ken Mattingly is awoken in the night and stuffed into the Flight Simulator</a> </div> ---- Apollo 13 - Flight Simulator {%youtube 2-p4XrzodbM %} <div style="font-size: 18px; text-align: left;"> <a href="https://www.youtube.com/watch?v=2-p4XrzodbM">Ken in the Simulator Apollo 13</a> </div> ---- ### Test Double replace a component on which the SUT depends with a "test-specific equivalent." ![](https://i.imgur.com/bMxQgQm.gif) source: [xUnit Patterns - Test Double](http://xunitpatterns.com/Test%20Double.html) ---- ### Types of Test Double ![](https://i.imgur.com/ktTZ2BT.gif =800x) source: [xUnit Patterns - Test Double](http://xunitpatterns.com/Test%20Double.html) --- <!-- .slide: style="font-size: 28px;" --> ### 單元測試二種外部依賴(1) ### 傳入依賴 + 非出口點的依賴關係。 + 它們並不代表該工作單元最終行為的需求。 + 它們只是在此處為工作單元提供特定於測試的專用資料或行為。 + 例如: - 數據庫查詢的結果, - 文件系統上文件的內容, - 網路的回應等。 + 請注意,這些是先前的系統運作的結果,是被動資料,向內流入工作單元。 ---- 傳入依賴 ![](https://i.imgur.com/6ddYBTY.png) ---- ### Test double-Stub ![](https://i.imgur.com/R5fwlfC.png) ---- 一個 Stub 範例 {%youtube b4a7qtQZvyQ %} ---- ### Test double-Stub - 用於打破傳入依賴,假的 modules, objects或是functions - 它們向 SUT 提供假行為或數據。 - 我們不會對它們做 assert,在一個單元測試中可能會有多個 stub。 參考資料:[The Art of Unit Testing, 3rd](https://www.manning.com/books/the-art-of-unit-testing-third-edition) ---- <!-- .slide: style="font-size: 28px;" --> ### Demo: pytest-Mock object (mock db) ### 練習 f1_stub_operation (20分鐘) + 使用 Mock 模擬外部相關函式回傳的資料 + 外部相關函式:手術清單 from DB ```python= def test_room601_operations_true(operation_data: List[lib.Operation], mocker: pytest_mock.MockFixture): mocker.patch('lib.get_operations_from_db', autospec=True, return_value=operation_data) room = '601' operations = lib.all_operations(date(2021, 7, 13), room) assert all(op.room == room for op in operations) ``` ---- ### another pytest-Mock object example ### mock request/rest api + DICOM list from request ---- <!-- .slide: style="font-size: 28px; text-align: left;" --> ### Mock datetime ### 預約疫苗 (production code), 15分鐘 + 實作一個 function book_vaccine() + 如果目前時間 > 2021/6/14 8:00, 回傳 "開始預約" + 否則回傳 "2021-06-14 08:00 才能預約" + Hint: python datetime lib <br> ### 單元測試 15分鐘 + 實作單元測試, 來驗證 production code 的邏輯是正確的 --- <!-- .slide: style="font-size: 28px;" --> ### 單元測試二種外部依賴(2) ### 傳出依賴 + 代表我們工作單元出口的依賴。 + 例如: - 調用記錄器, - 將某些內容保存到數據庫, - 發送電子郵件, - 向Webhook通知API發生了什麼事情等。 + 請注意,這些都是動詞:“調用”,“發送”和“通知” ---- 傳出依賴 ![](https://i.imgur.com/ugye82X.png) ---- ### Test double-Mock ![](https://i.imgur.com/lwKO2E3.png) ---- ### Test double-Mock ![](https://i.imgur.com/m8ArZIQ.png) ---- ### Test double-Mock - 用於打破輸出依賴。 - Mock 代表單元測試中的出口點。 - 用驗證 SUT 和 Mock 的互動方式來判斷測試是否通過 - 通常每個測試只會有一個 Mock 以保持測試碼的可維護性和可讀性。 參考資料:[The Art of Unit Testing, 3rd](https://www.manning.com/books/the-art-of-unit-testing-third-edition) ---- 互動方式測試 + 檢查工作單元(SUT)如何跟 DOC 進行互動或傳送訊息給 DOC (超出其控制範圍的依賴項),也就是「調用函數」。 + Mock function 或 object 是用來記錄互動過程 + 以便在單元測試程式碼中作驗證對外部依賴項的調用是正確的 (assert)。 ---- ### mock object to verify interaction ```python= def test_electric_guitars(guitar_data: List[lib.Guitar], mocker: pytest_mock.MockFixture): # uncomment the following 2 line to mock the external dependency mocker.patch('lib.get_guitars_from_db', autospec=True, return_value=guitar_data) mock_log = mocker.patch('lib.log', autospec=True) style = 'electric' guitars = lib.all_guitars(style) # sweet little generator expression assert all(g.style == style for g in guitars) # uncomment the following line to test the external dependency behave as design mock_log.assert_called_once_with(f"Guitars for {style}") ``` ---- ### case study (reportlab) + how to verify the generated pdf is right? --- ### Code Coverage (程式碼覆葢率) + 軟體測試中的一種度量 + 描述程式中原始碼被測試的比例 ---- + coverage.py is the preferred Python coverage tool that measures code coverage. + pytest-cov that will allow you to call coverage.py from pytest + install coverage.py ```shell= pip install pytest-cov ``` + run pytest with coverage ```shell= pytest tests.py --cov=. pytest tests.py --cov=. --cov-report=html pytest tests.py --cov=. --cov-report=xml ``` ---- 思考:覆蓋率是否愈多愈好? ---- ```python= def foo(x: int, y: int) -> int: z = 0 if (x > 0) and (y > 0): z = x return z ``` + 函式覆蓋率:只要函式foo有執行過一次,即滿足函式覆蓋率100%的條件。 + 指令覆蓋率:若有呼叫過foo(1,1),函式中每一行(包括z = x;)都執行一次,滿足指令覆蓋率100%的條件。 <br/> <div style="font-size: 20px; text-align: left;"> <a href="https://bit.ly/3xPaMhq">Source: 維基百科:代碼覆蓋率</a> </div> ---- ```python= def foo(x: int, y: int) -> int: z = 0 if (x > 0) and (y > 0): z = x return z ``` + 判斷覆蓋率: - foo(1,1)會使if的條件成立,因此z = x 會執行 - foo(0,1)會使if的邏輯運算式(x>0) and (y>0) 不成立 - 因此滿足判斷覆蓋率100%的條件。 <br/> <div style="font-size: 20px; text-align: left;"> <a href="https://bit.ly/3xPaMhq">Source: 維基百科:代碼覆蓋率</a> </div> ---- ```python= def foo(x: int, y: int) -> int: z = 0 if (x > 0) and (y > 0): z = x return z ``` + 條件覆蓋率: - foo(1,1)、foo(1,0) 會使(x>0)的條件成立, - foo(0,0)會使(x>0)的條件不成立 - foo(1,1)會使(y>0)的條件成立, - foo(1,0)、foo(0,0)會使(y>0)條件不成立 - 所有條件都有出現成立及不成立的情形,因此滿足條件覆蓋率100%的條件。 <br/> <div style="font-size: 20px; text-align: left;"> <a href="https://bit.ly/3xPaMhq">Source: 維基百科:代碼覆蓋率</a> </div> ---- + 以 ROI 考慮單元測試覆蓋率:先要找有商業邏輯判斷的程式碼 + [Code Coverage 導入團隊的 3 個步驟 by 91教練](https://tdd.best/blog/code-coverage-and-tdd/) + Code coverage > 0 %:至少要有測試 + 相對趨勢 > 絕對數字:每次 commit 的 code coverage 不要下降 + 頻繁簽入:接著鼓勵大家頻繁簽入,觸發自動化的迴歸測試,享受持續整合的好處。 --- ### 參考資料 - [Python Testing with pytest](https://pragprog.com/titles/bopytest/python-testing-with-pytest/) - [The Art of Unit Testing, 3rd](https://www.manning.com/books/the-art-of-unit-testing-third-edition) - xUnit Patterns: [Mocks, Fakes, Stubs and Dummies](http://xunitpatterns.com/Mocks,%20Fakes,%20Stubs%20and%20Dummies.html)
{"metaMigratedAt":"2023-06-16T03:50:04.814Z","metaMigratedFrom":"YAML","title":"使用 Pytest進行單元測試(2)","breaks":true,"slideOptions":"{\"transition\":\"slide\",\"showNotes\":false,\"incremental\":true}","contributors":"[{\"id\":\"89a0ce2f-418b-48fd-9391-f1e7c3fdd209\",\"add\":10586,\"del\":4071}]"}
    951 views