## 使用 Pytest 進行單元測試 <br/> ### Max Lai --- I hear and I forget. I see and I remeber. I do and I understand. --- ### 今天的 demo code https://github.com/cclai999/pytest-0706 --- ## 為什麼需要寫測試呢? + 證明程式碼是依照我想的一樣地在執行 <!-- .element: class="fragment" data-fragment-index="1" --> - 手動驗證程式,非常麻煩 - 不能確定每次測試方式都是相同的 + 避免改A壞B <!-- .element: class="fragment" data-fragment-index="2" --> + 為了將來修改程式時設置一個安全網 + 給其他人一個如何使用自己寫的程式的一個使用範例 <!-- .element: class="fragment" data-fragment-index="3" --> --- ## 執行環境準備(PyCharm Setup for Pytest) + 安裝 Miniconda + 安裝 PyCharm + 安裝 Pytest [PyCharm Setup for Pytest 執行環境準備](https://hackmd.io/@cclai999/pycharm-setup) ---- ### Install pytest 建立 PyCharm 專案後,在 Terminal 用 pip 安裝 ```bash pip install pytest ``` ##### Pytest 官方文件:https://docs.pytest.org/ ---- ### 測試 Pytest 是否能正確執行 ```python= import math def test_sqrt(): num = 25 assert math.sqrt(num) == 5 def test_square(): num = 7 assert 7 * 7 == 40 # 49 # assert 7 * 7 == 49 # 49 def test_equality(): # assert 10 == 11 # 10 assert 10 != 11 # 10 ``` --- ## demo project + clone project@github ```shell= git clone git@github.com:cclai999/pytest-0706.git conda create --name pytestlab python=3.8 ``` + 在 PyCharm 將專案的 interpreter 設定為 pytestlab --- I hear and I forget. I see and I remeber. I do and I understand. --- ## unit test vs pytest ---- ### 第一個 unit test hello.py ```python= def hello_name(name): return f'Hello {name}' if __name__ == '__main__': print(hello_name("Max")) ``` ---- ### unittest test_hello_unittest.py ```python= import unittest from hello import hello_name class TestHello(unittest.TestCase): def test_hello_name(self): self.assertEqual(hello_name('Max'), 'Hello Max') ``` 1. import unittest 2. 建立 TestHello (繼承 unittest.TestCase) 3. 在 TestHello 為每一個測試案何實作一個 method 4. 使用 self.assert* 來進行 assertions ---- ## 執行 unittest ```shell python -m unittest test_hello_unittest.py . ---------------------------------------------------------------------- Ran 1 test in 0.000s OK ``` ---- <!-- .slide: style="font-size: 36px;" --> ### pytest test_hello_pytest.py ```python= from hello import hello_name def test_hello_name(): assert hello_name('Max') == 'Hello Max' ``` 1. Pythonic 2. 支援 unittest test case 3. 只需要 ==assert==,不需要記 ==assert.*== (ex. assertEqual) 4. 更多的進階功能 (e.g., fixture, mark, parametrize and etc.) 5. 強大的套件 ---- ## 執行 pytest ```shell $ pytest test_hello_pytest.py ``` ![](https://i.imgur.com/V3RxYe3.png) =="F"== 表示測試沒有通過,如果出現 =="."== 則表示成功 ---- ## 執行 pytest ```shell $ pytest -v test_hello_pytest.py ``` ![](https://i.imgur.com/Of2dcMh.png) ---- ## pytest fail message ```shell $ pytest test_hello_pytest.py ``` ![](https://i.imgur.com/0Eg6xJZ.png) =="F"== 表示測試沒有通過,如果出現 =="."== 則表示成功 ---- ## pytest fail message ```shell $ pytest -v test_hello_pytest.py ``` ![](https://i.imgur.com/QRhwiWC.png) ---- ### 何謂單元測試(1/2) <div style="text-align: left;"> [Wikipedia 的定義](https://en.wikipedia.org/wiki/Unit_testing) Unit tests are typically <span style="color:red">automated tests</span> written and run by software developers to ensure that a section of an application (known as the <span style="color:red">"unit"</span>) meets its design and behaves as intended. In procedural programming, a unit could be an entire module, but it is more commonly an individual <span style="color:red">function or procedure</span>. </div> <div style="font-size: 18px; text-align: left;"> <a href="https://www.manning.com/books/the-art-of-unit-testing-third-edition">Source: The Art of Unit Testing, 3rd</a> </div> ---- ### 何謂單元測試(2/3) <div class="r-stack" style="text-align: left;"> A unit test is an automated piece of code that <font color=#FF0000>invokes</font> the unit of work trough an <font color=#FF0000>entry point</font>, and then <font color=#FF0000>checks</font> one of its <font color=#FF0000>exit points</font>. A unit test is almost always written using a unit testing framework. It can be written easily and runs quickly. It’s <font color=#FF0000>trustworthy, readable and maintainable</font>. It is <font color=#FF0000>consistent</font> as long as the production code we control has not changed. </div> <div style="font-size: 18px; text-align: left;"> <a href="https://www.manning.com/books/the-art-of-unit-testing-third-edition">Source: The Art of Unit Testing, 3rd</a> </div> ---- ### 何謂單元測試(3/3) <div class="r-stack" style="text-align: left;"> 單元測試是一段自動化程式碼,這段程式碼會<font color=#FF0000>呼叫</font>被測試的工作單元之<font color=#FF0000>入口點</font>,然後<font color=#FF0000>檢查</font>其中一個<font color=#FF0000>出口點</font>。單元測試幾乎總是使用單元測試框架編寫的。它可以輕鬆地被編寫並且能夠快速運行。它是<font color=#FF0000>可信賴的,可讀的以及可維護的</font>。只要我們產品程式碼沒有改變,單元測試的<font color=#FF0000>執行結果</font>就應該是<font color=#FF0000>穩定一致的</font>。 </div> <div style="font-size: 18px; text-align: left;"> <a href="https://www.manning.com/books/the-art-of-unit-testing-third-edition">Source: The Art of Unit Testing, 3rd</a> </div> ---- ### PyCharm Demo ### 練習 test_hello.py (10分鐘) --- ### 三種測試的出口驗證 ---- #### 名詞解釋-SUT <div style="text-align: left;"> SUT: Sytem Under Test, 被測試系統 有些人會稱為 CUT (Class Under Test or Code Under Test) </div> ---- ### 1st SUT 驗證 + 進入點(entry point): sum(a,b) + 出口點(exit point): return value ![](https://i.imgur.com/fSQjPnL.png) ```python= def sum(a,b): result = int(a) + int(b) return result ``` ---- ### 2st SUT 驗證 + 進入點(entry point): M+(a) + 出口點(exit point): MR(有時並沒有外部可存取的 getter) ![](https://i.imgur.com/KpAtNyS.png =800x) ---- ### 3rd SUT 驗證 + 進入點(entry point): check(體溫) + 出口點(exit point): disp(msg), 在電子看板秀警告文句 ![](https://i.imgur.com/N4qkcHn.png) --- ### pytest-如何測試 exception 的狀況 production code (calculator.py) ```python= def divide(self, a, b): """Divide two numbers.""" try: return a / b except ZeroDivisionError as ex: raise CalculatorError("You can't divide by zero.") from ex ``` ---- ### pytest-如何測試 exception 的狀況 test code (test_calculator.py) ```python= def test_divide_by_zero(): calculator = Calculator() # result = calculator.divide(9, 0) with pytest.raises(CalculatorError): result = calculator.divide(9, 0) ``` ---- ### File names and function names * By default pytest only identifies the file names starting with ==test_== or ending with ==_test== as the test files. * Pytest requires the test method names to start with =="test"== . ---- ### PyCharm Demo ### 練習 test_calculator.py (15分鐘) + test_divide_by_zero() + 請為 calculator.py 撰寫其他 function 的 unit test --- ### 單元測試的命名 + Test name should express a specific requirement + [UnitOfWork_StateUnderTest_ExpectedBehavior] + example + test_isAdult_AgeLessThan18_False ---- ### pytest-Parametrizing tests production code (triangle.py) ```python= def type_of_triangle(a, b, c): if not is_valid_triangle(a, b, c): return "不是三角形" if (a == b) and (b == c): return "等邊三角形" elif (a == b) or (b == c) or (a == c): return "等腰三角形" elif (a * a + b * b == c * c) or (a * a + c * c == b * b) or (c * c + b * b == a * a): return "直角三角形" else: return "一般三角形" ``` ---- ### pytest-Parametrizing tests test code (test_triangle.py) ```python= @pytest.mark.parametrize("a,b,c,expected", [ (0, 1, 3, "不是三角形"), (1, 1, 1, "等邊三角形"), (2, 2, 3, "等腰三角形"), (3, 4, 5, "直角三角形"), (4, 5, 6, "一般三角形") ]) def test_type_of_triangle(a, b, c, expected): assert expected == type_of_triangle(a, b, c) ``` ---- ### PyCharm Demo ### 練習 test_triangel.py (15分鐘) + @pytest.mark.parametrize + 請為 triangle.py 撰寫其他 function 的 unit test --- ### 3A 原則 一個單元測試通常包含了三個行為 1. Arrage:準備物件、建立物件、進行物件必要的設定; 2. Act:操作物件; 3. Assert:驗證某件事符合預期 ---- ### pytest-Fixtures 在單元測試之前先建立好變數或物件,可重覆利用 ```python= @pytest.fixture() def some_data(): return 42 def test_some_data(some_data): """Return value for fixture.""" assert some_data == 42 def test_inc_data(some_data): """Use fixture return value in a test.""" inc_data = some_data + 1 assert inc_data == 43 ``` ```shell= pytest -v test_fixtures.py ``` ---- ### pytest-Fixtures + 使用 Fixtures 來為單元測試作 setup/cleanup + PyCharm Demo + 練習 test_todo.py (15分鐘) ---- ### pytest-Fixtures: scope ###### controls how often a fixture gets set up and torn down. ```shell= pytest -v --setup-show test_scope.py ``` ![](https://i.imgur.com/PdIXpIz.png =800x) ---- ### pytest-Fixtures: autouse ###### get a fixture to run all of the time ```shell= pytest -v -s test_autouse.py ``` ![](https://i.imgur.com/s7NF339.png) ---- ### pytest-Fixtures: yield + The code before the yield runs before each test; + The code after the yield runs after the test. ```shell= pytest -v -s test_yield.py ``` ![](https://i.imgur.com/iuR3LQk.png) --- ## 優秀單元測試的特性: + 它應該運行得很快。 + 它總是會得到相同的結果(如果你沒有更動產品程式碼)。 + 它跟其他單元測試應該是完全獨立。 + 它應該在不需要作業系統檔、網絡、資料庫的情況就能運行。 --- ### 參考資料 - [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) - [一次搞懂單元測試、整合測試、端對端測試之間的差異](https://blog.miniasp.com/post/2019/02/18/Unit-testing-Integration-testing-e2e-testing) - [Python Table Manners - 測試 (一)](https://bit.ly/3jH9UqU) - [Python Table Manners - 測試 (二)](https://bit.ly/3dGcXMc)
{"metaMigratedAt":"2023-06-16T03:42:31.145Z","metaMigratedFrom":"YAML","title":"使用 Pytest進行單元測試","breaks":true,"slideOptions":"{\"transition\":\"slide\",\"showNotes\":false,\"incremental\":true}","contributors":"[{\"id\":\"89a0ce2f-418b-48fd-9391-f1e7c3fdd209\",\"add\":18224,\"del\":8683}]"}
    2102 views