## 使用 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."

source: [xUnit Patterns - Test Double](http://xunitpatterns.com/Test%20Double.html)
----
### Types of Test Double

source: [xUnit Patterns - Test Double](http://xunitpatterns.com/Test%20Double.html)
---
<!-- .slide: style="font-size: 28px;" -->
### 單元測試二種外部依賴(1)
### 傳入依賴
+ 非出口點的依賴關係。
+ 它們並不代表該工作單元最終行為的需求。
+ 它們只是在此處為工作單元提供特定於測試的專用資料或行為。
+ 例如:
- 數據庫查詢的結果,
- 文件系統上文件的內容,
- 網路的回應等。
+ 請注意,這些是先前的系統運作的結果,是被動資料,向內流入工作單元。
----
傳入依賴

----
### Test double-Stub

----
一個 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發生了什麼事情等。
+ 請注意,這些都是動詞:“調用”,“發送”和“通知”
----
傳出依賴

----
### Test double-Mock

----
### Test double-Mock

----
### 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}]"}