owned this note
owned this note
Published
Linked with GitHub
---
title: 使用 Pytest進行單元測試
tags: python, pytest, test, unit test
slideOptions:
transition: slide
showNotes: false
incremental: true
---
## 使用 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
```

=="F"== 表示測試沒有通過,如果出現 =="."== 則表示成功
----
## 執行 pytest
```shell
$ pytest -v test_hello_pytest.py
```

----
## pytest fail message
```shell
$ pytest test_hello_pytest.py
```

=="F"== 表示測試沒有通過,如果出現 =="."== 則表示成功
----
## pytest fail message
```shell
$ pytest -v test_hello_pytest.py
```

----
### 何謂單元測試(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

```python=
def sum(a,b):
result = int(a) + int(b)
return result
```
----
### 2st SUT 驗證
+ 進入點(entry point): M+(a)
+ 出口點(exit point): MR(有時並沒有外部可存取的 getter)

----
### 3rd SUT 驗證
+ 進入點(entry point): check(體溫)
+ 出口點(exit point): disp(msg), 在電子看板秀警告文句

---
### 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
```

----
### pytest-Fixtures: autouse
###### get a fixture to run all of the time
```shell=
pytest -v -s test_autouse.py
```

----
### 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
```

---
## 優秀單元測試的特性:
+ 它應該運行得很快。
+ 它總是會得到相同的結果(如果你沒有更動產品程式碼)。
+ 它跟其他單元測試應該是完全獨立。
+ 它應該在不需要作業系統檔、網絡、資料庫的情況就能運行。
---
### 參考資料
- [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)