Successfully reported this slideshow.

使用 Pytest 進行單元測試 (PyCon TW 2021)

1

Share

1 of 90
1 of 90

More Related Content

Related Books

Free with a 14 day trial from Scribd

See all

使用 Pytest 進行單元測試 (PyCon TW 2021)

  1. 1. Max Lai 使用 Pytest 進行單元測試
  2. 2. About Me • Max Lai • 童綜合醫院資訊部高級管理師 • 負責AI醫療影像技術研發 • 社群經歷 • 2014年6月共同發起「Taichung.py 」 Meetup • 2014年12月發起並主持「台中敏捷社群」 活動 • 2015~2018年「台中敏捷之旅研討會 」主辦人 • PyCon TW 2018, 2020 投稿分享
  3. 3. 今天的 demo code https://github.com/cclai999/pycontw21-pytest
  4. 4. 為什麼需要寫測試呢? • 證明程式碼是依照自己想的一樣地在執行 • 手動驗證程式,非常麻煩,而且不能確定每次測試方式都是相同的 • 有了測試程式就能夠每次都執行相同的驗證條件 • 避免改A壞B • 為了將來修改程式時設置一個安全網 • 給其他人一個如何使用自己程式的一個範例
  5. 5. 執行環境準備(PyCharm Setup for Pytest) • 安裝 Miniconda • 安裝 PyCharm • 安裝 Pytest
  6. 6. 安裝 Miniconda • Windows 安裝說明, https://bit.ly/38pBIcr • macOS 安裝說明, https://bit.ly/3sXiOTN • Linux(Ubuntu) 安裝說明, https://bit.ly/3jqnxdk
  7. 7. 安裝 PyCharm • 下載 PyCharm https://www.jetbrains.com/pycharm/download/
  8. 8. PyCharm Setup for Pytest(1/9) 建立新專案
  9. 9. PyCharm Setup for Pytest(2/9) 設定新專案名稱: “pytestlab”
  10. 10. PyCharm Setup for Pytest(3/9) 設定 default test runner
  11. 11. PyCharm Setup for Pytest(4/9) 設定 default test runner (cont.)
  12. 12. PyCharm Setup for Pytest(5/9) pip install pytest 建立專案後在 Terminal 用 pip 安裝 pytest
  13. 13. PyCharm Setup for Pytest(6/9) Create test file.
  14. 14. PyCharm Setup for Pytest(7/9) 測試 Pytest 是否能正確執行
  15. 15. PyCharm Setup for Pytest(8/9) 執行 Pytest
  16. 16. PyCharm Setup for Pytest(9/9) test pass!!!
  17. 17. Demo Project • clone project@github • 在 PyCharm 中將專案的 interpreter 設定為 pytestlab git clone git@github.com:cclai999/pytest-2021.git conda create --name pytestlab python=3.8
  18. 18. unit test vs pytest
  19. 19. 第一個 unit test hello.py
  20. 20. unittest test_hello_unittest.py 1. import unittest 2. 建立 TestHello class (繼承 unittest.TestCase) 3. 在 TestHello 為每一個測試案例實作一個 method 4. 使用 self.assert* 來進行 assertions More: list of unittest assert methods
  21. 21. 執行 unittest python -m unittest test_hello_unittest.py . -------------------------------------------------- Ran 1 test in 0.000s OK
  22. 22. pytest test_hello_pytest.py 1. Pythonic 2. 支援 unittest test case 3. 只需要 assert,不需要記 assert* (ex. assertEqual) 4. 更多的進階功能 (e.g., fixture, mark, parametrize and etc.) 5. 強大的套件
  23. 23. 執行 pytest $ pytest test_hello_pytest.py “F” 表示測試沒有通過,如果出現 “.” 則表示成功
  24. 24. 執行 pytest $ pytest -v test_hello_pytest.py
  25. 25. pytest fail message
  26. 26. pytest test 命名規則 • Test file name: recognizes test_*.py or *_test.py as the test files. • Test function name: requires the test method names to start with “test”.
  27. 27. PyCharm Demo test hello.py
  28. 28. 何謂單元測試 Wikipedia 的定義 • Unit tests are typically automated tests written and run by software developers • to ensure that a section of an application (known as the “unit”) • meets its design and behaves as intended. • In procedural programming, a unit could be an entire module, but it is more commonly an individual function or procedure. source: https://en.wikipedia.org/wiki/Unit_testing
  29. 29. 何謂單元測試(cont.) • 單元測試是一段自動化程式碼, • 這段程式碼會呼叫被測試的工作單元之入口點, • 然後檢查其中一個出口點。 • 單元測試大多數是使用單元測試框架編寫的。 • 它可以輕鬆地被編寫並且能夠快速運行。 • 它是可信賴的,可讀的以及可維護的。 • 只要我們產品程式碼沒有改變,單元測試的執行結果就應該是穩定 一致的。 source: The Art of Unit Testing, 3rd
  30. 30. 三種單元測試的出口驗證
  31. 31. 1st SUT 出口驗證 • 進入點(entry point): sum(a,b) • 出口點(exit point): return value SUT sum(a, b) return value • SUT: Sytem Under Test, 被測試系統 • 有些人會稱為 CUT (Class Under Test or Code Under Test)
  32. 32. 1st SUT exit 驗證(cont.) Production code Test code
  33. 33. 2nd SUT 出口驗證 • 進入點(entry point): M+(a) • 出口點(exit point): MR(有時並沒有外部可存取的 getter) SUT M+ Calculator [total] MR
  34. 34. 3rd SUT 出口驗證 • 進入點(entry point): check(體溫) • 出口點(exit point): disp(msg), 在電子看板秀警告文句 SUT 體溫 電子 看板 3rd party lib
  35. 35. 如何測試 exception 的狀況(1/3) production code (calculator.py)
  36. 36. 如何測試 exception 的狀況(2/3) test code pass (test_calculator.py)
  37. 37. 如何測試 exception 的狀況(2/3) test code fail (test_calculator.py)
  38. 38. PyCharm Demo c_exception/test_calculator.py
  39. 39. 參數化測試 production code (triangle.py)
  40. 40. 參數化測試 test code (test_triangle.py)
  41. 41. PyCharm Demo d_parametrize/test_triangle.py
  42. 42. 第一階段 Q&A
  43. 43. 使用 Pytest 進行單元測試 Part 2
  44. 44. 3A 原則 一個單元測試通常包含了三個行為 1. Arrange:準備物件、建立物件、進行物件必要的設定; 2. Act:操作物件; 3. Assert:驗證執行結果符合預期
  45. 45. Fixture 在單元測試之前先建立好變數或物件,可重覆利用
  46. 46. $ pytest -v --setup-show test_fixtures.py test_fixtures.py::test_some_data SETUP F some_data test_fixtures.py::test_some_data (fixtures used: some_data)PASSED TEARDOWN F some_data test_fixtures.py::test_inc_data SETUP F some_data test_fixtures.py::test_inc_data (fixtures used: some_data)PASSED TEARDOWN F some_data ================================ 3 passed in 0.07s ============================= Fixture 的 SETUP & TEARDOWN
  47. 47. fixture — yield • The code before the yield runs before each test; • The code after the yield runs after the test. $ pytest -v -s test_yield.py test_yield.py::test Pre condition Body of test PASSEDPost condition #1 #2 #3
  48. 48. fixture — conftest.py 在測試的Path下,將 fixture 存在 conftest.py 文件,進行全局配置 test/conftest.py 檔案中的 fixture可以給 test_a.py, test_b.py, test_c.py, 以及 test_d.py 中使用 test/sub/conftest.py 只可以給 test_c.py, 以及 test_d.py 中使用 tests ├── conftest.py ├── test_a.py ├── test_b.py └── sub ├── __init__.py ├── conftest.py ├── test_c.py └── test_d.py
  49. 49. PyCharm Demo e_fixture/test_fixtures.py e_fixture/test_yield.py
  50. 50. 二種單元測試的外部依賴
  51. 51. 優秀單元測試的特性: • 它應該運行得很快。 • 它總是會得到相同的結果(如果你沒有更動產品程式碼)。 • 它跟其他單元測試應該是完全獨立。 • 它應該在不需要作業系統檔、網路、資料庫的情況就能運行。
  52. 52. 外部依賴的可能問題 • SUT:System Under Test或Software Under Test,待測程式。 • DOC:Depended-on Component(相依元件)。DOC是SUT 執行的時候會使用到的元件。 問題 • 當SUT所依賴的模組(DOC)無法被呼叫時,我們如何獨立驗證 SUT 的邏輯? • 我們如何避免測試執行太慢? source: xUnit Patterns - Test Double
  53. 53. Test Double (測試替身) • 當SUT所依賴的 DOC 用一個 “特定於測試的等效項” 來取代 • 特定於測試的等效項: test-specific equivalent, Test Double source: xUnit Patterns - Test Double
  54. 54. Types of Test Double source: xUnit Patterns - Test Double
  55. 55. 1st 外部依賴 – 傳入依賴 • 非出口點的依賴關係 • 它們並不代表該工作單元最終行為 • 它們只是在此處為工作單元提供特定於測試的專用資料或行為 • 例如: • 資料庫查詢的結果 • 文件系統上文件的內容 • 網路的回應等 • 這些是先前的系統運作的結果, 是被動資料,向內流入工作單元。 SUT Test 出口點 DOC 入口點 Data or behavior 傳入依賴
  56. 56. 使用 STUB 隔離傳入依賴 • 利用假的 modules, objects 或 functions 向 SUT 提供假行為或數據 • 我們不會對它們做 assert • 在一個單元測試中可能會有多個 stub SUT Test 出口點 DOC 入口點 特定的 Data or behavior STUB
  57. 57. 傳入依賴範例 – get 手術清單 from db 產生 exception production code test code 外部相關函式
  58. 58. 傳入依賴範例 – get 手術清單 from db
  59. 59. 使用 Mock Object 模擬外部相關函式回傳資料 – fixture – mocker
  60. 60. pytest-mock plug-in • 提供 mocker fixture • 為 unittest.mock 提供一個較簡單的 API 封裝 • Install: pip install pytest-mock
  61. 61. 2nd 外部依賴 – 傳出依賴 • 工作單元出口的依賴關係 • 例如: • 呼叫 logger, • 將某些內容保存到資料庫, • 發送電子郵件, • 向Webhook API push message • 這些都是動詞: • “呼叫”,“發送” 和 “通知” SUT Test 出口點 DOC 入口點 傳出依賴
  62. 62. 使用 Mock 打破輸出依賴 • Mock 代表單元測試中的出口點。 • 以驗證 SUT 和 Mock 的互動方式來判斷測試是否通過 • 通常每個測試只會有一個 Mock 以保持測試碼的可維護性和可讀性 SUT Test DOC 入口點 MOCK 記錄互動 出口點
  63. 63. 如何測試互動方式 • 檢查工作單元(SUT)如何呼叫DOC函數 • 以 Mock function/object 來記錄互動過程 • 在單元測試程式碼中檢查SUT對DOC的呼叫是正確的 (assert)
  64. 64. 多個 Stub vs 一個 Mock SUT Test DOC 入口點 MOCK 記錄互動 出口點 DOC STUB DOC STUB
  65. 65. 輸出依賴範例 – 驗證正確呼叫 log production code test code 如何驗證寫入log?
  66. 66. 使用 Mock Object 確認是否傳入正確參數 – mocker:驗證傳入參數是否正確以及僅呼叫一次 • assert_called() • assert_called_with() • assert_any_call() • … source: python doc test_lib_mock.py::test_room602_operations_log PASSED [100%] ======================== 1 passed in 0.03s =========================
  67. 67. 使用 mocker.spy 記錄互動並且呼叫DOC – mocker:驗證傳入參數是否正確以及僅呼叫一次 test_lib_mock.py::test_room602_operations_log_with_spy PASSED [100%] 實務上應該會 log 到檔案,不會在這邊看到。 msg: Query for room-'602'. ============================ 1 passed in 0.03s ==============================
  68. 68. PyCharm Demo f_stub_mock_operation
  69. 69. 第二階段 Q&A
  70. 70. 使用 Pytest 進行單元測試 Part 3
  71. 71. 整合測試 • 有時候單一模組完全通過單元測試 • 但與其他模組互動時,還是有可能發生錯誤 • 右圖是兩個元件整合時所發生的問題 • 整合測試便是要確保 模組與模組之間的互動行為是正確的
  72. 72. 測試金字塔 source: The Practical Test Pyramid
  73. 73. flask-todos Flask demo for RESTful API for Todo demo flask-dodos ├── app │ ├── __init__.py │ ├── models.py │ └── routes.py ├── test │ ├── conftest.py │ └── test_todos.py ├── config.py ├── main.py └── requirements.txt HTTP Method Action Examples GET 取得資源的所有資料 http://[hostname]/api/tasks 取得待辦事項清單 GET 取得資源的特定資料 http://[hostname]/api/tasks/1 取得編號-1的待辦事項 POST 新增一筆資料 http://[hostname]/api/tasks 提供細節資料以新增待辦事項 PUT 更新某一待辦事項資料 http://[hostname]/api/tasks/1 更新編號-1待辦事項的資料 DELETE 刪除某一待辦事項 http://[hostname]/api/tasks/1 刪除更新編號-1待辦事項
  74. 74. flask-todos – fixture(1) 運作工廠模式每一個單元測試都重新建立一個 DB conftest.py #1 Flask 工廠模式傳入 ENV=’Test’ #2載入 TestConfig
  75. 75. flask-todos – fixture(2) 每一個單元測試都重新建立一個 DB conftest.py 1) 建立 DB 資料表 2) 插入二筆資料 3) 轉換至單元測試 4) 刪除資料庫表格 #1 #2 #3 #4
  76. 76. flask-todos – test_todos.py # 二個單元測試有獨立DB # 不會互相干擾 source: Testing Flask Applications
  77. 77. $ pytest -v --setup-show test/test_todos.py test/test_todos.py::test_add_task_should_return_task3 SETUP F client test/test_todos.py::test_add_task_should_return_task3 (fixtures used: client)PASSED TEARDOWN F client test/test_todos.py::test_query_tasks_with_2_rows_should_return_2_tasks SETUP F client test/test_todos.py::test_query_tasks_with_2_rows_should_return_2_tasks (fixtures used: client)PASSED TEARDOWN F client test/test_todos.py::test_query_task3_with_2_rows_should_return_error SETUP F client test/test_todos.py::test_query_task3_with_2_rows_should_return_error (fixtures used: client)PASSED TEARDOWN F client ================================ 3 passed in 0.07s =============================
  78. 78. pytest plug-in • pytest-mock: mock object • pytest-cov: code coverage • pytest-freezegun: free time more: Awesome pytest
  79. 79. Code Coverage (程式碼覆蓋率) • 軟體測試中的一種度量 • 描述程式中原始碼被測試的比例 • coverage.py 是最常被用來量測 code coverage 的工具 • pytest-cov 則是可以在 pytest 呼叫 coverage.py 的 plug in
  80. 80. Code Coverage (程式碼覆蓋率) • install coverage.py • run pytest with coverage pip install pytest-cov pytest tests.py --cov=. pytest tests.py --cov=. --cov-report=html pytest tests.py --cov=. --cov-report=xml
  81. 81. Coverage report
  82. 82. Coverage report
  83. 83. 覆蓋率是否愈多愈好? • 以 ROI 考慮單元測試覆蓋率: 先要找有商業邏輯判斷的程式碼 • 相對趨勢 > 絕對數字: 每次 commit 的 code coverage 不要下降
  84. 84. 如何 Mock 系統時間 • 情境:預約疫苗 • 實作一個 function book_vaccine() • 如果目前時間 > 2021/6/14 8:00, 回傳 “開始預約” • 否則回傳 “2021-06-14 08:00 才能預約”
  85. 85. 預約疫苗 (Mock系統時間)
  86. 86. 系統時間無法簡單地使用 mock object E AttributeError: <module 'datetime' from '/Users/maxlai/miniconda3/envs/pytestlab/lib/python3.8/datetime.py’> does not have the attribute 'now'
  87. 87. pytest-freezegun: 在測試時固定系統時間. 安裝: pip install pytest-freezegun
  88. 88. 重點回顧 單元測試 • What?Why?How? • 設置一個安全網 三種出口測試 二種外部依賴 • Mock Object 3A 原則 • Arrange: fixture; Act; Assert Flask 整合測試 • 測試金字塔:#單元測試 > #整合測試 • fixture; yield; app.test_client() 程式碼覆蓋率 • 考慮 ROI • 相對趨勢 > 絕對數字 Plugins • coverage (pytest-cov) • system time (pytest-freezegun) • Requests (requests-mock) • and others.
  89. 89. 參考資料 • Python Testing with pytest • The Art of Unit Testing, 3rd • Testing Flask Applications • 15 amazing pytest plugins • Python Table Manners - 測試 (一)
  90. 90. Thanks Taichung.py

×