使用 Pytest 進行單元測試


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


為什麼需要寫測試呢?

  • 證明程式碼是依照我想的一樣地在執行
    • 手動驗證程式,非常麻煩
    • 不能確定每次測試方式都是相同的
  • 避免改A壞B
    • 為了將來修改程式時設置一個安全網
  • 給其他人一個如何使用自己寫的程式的一個使用範例

執行環境準備(PyCharm Setup for Pytest)

  • 安裝 Miniconda
  • 安裝 PyCharm
  • 安裝 Pytest

PyCharm Setup for Pytest 執行環境準備


Install pytest

建立 PyCharm 專案後,在 Terminal 用 pip 安裝

pip install pytest
Pytest 官方文件:https://docs.pytest.org/

測試 Pytest 是否能正確執行

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

def hello_name(name): return f'Hello {name}' if __name__ == '__main__': print(hello_name("Max"))

unittest

test_hello_unittest.py

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

python -m unittest test_hello_unittest.py 
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK


pytest

test_hello_pytest.py

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

$ pytest test_hello_pytest.py

"F" 表示測試沒有通過,如果出現 "." 則表示成功


執行 pytest

$ pytest -v test_hello_pytest.py


pytest fail message

$ pytest test_hello_pytest.py

"F" 表示測試沒有通過,如果出現 "." 則表示成功


pytest fail message

$ pytest -v test_hello_pytest.py


何謂單元測試(1/2)

[Wikipedia 的定義](https://en.wikipedia.org/wiki/Unit_testing)

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: The Art of Unit Testing, 3rd

何謂單元測試(2/3)

A unit test is an automated piece of code that invokes the unit of work trough an entry point, and then checks one of its exit points. A unit test is almost always written using a unit testing framework. It can be written easily and runs quickly. It’s trustworthy, readable and maintainable. It is consistent as long as the production code we control has not changed.
Source: The Art of Unit Testing, 3rd

何謂單元測試(3/3)

單元測試是一段自動化程式碼,這段程式碼會呼叫被測試的工作單元之入口點,然後檢查其中一個出口點。單元測試幾乎總是使用單元測試框架編寫的。它可以輕鬆地被編寫並且能夠快速運行。它是可信賴的,可讀的以及可維護的。只要我們產品程式碼沒有改變,單元測試的執行結果就應該是穩定一致的
Source: The Art of Unit Testing, 3rd

PyCharm Demo

練習 test_hello.py (10分鐘)


三種測試的出口驗證


名詞解釋-SUT

SUT: Sytem Under Test, 被測試系統

有些人會稱為 CUT (Class Under Test or Code Under Test)


1st SUT 驗證

  • 進入點(entry point): sum(a,b)
  • 出口點(exit point): return value
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)

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)

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)

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)

@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

在單元測試之前先建立好變數或物件,可重覆利用

@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
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.
pytest -v --setup-show test_scope.py


pytest-Fixtures: autouse

get a fixture to run all of the time
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.
pytest -v -s test_yield.py


優秀單元測試的特性:

  • 它應該運行得很快。
  • 它總是會得到相同的結果(如果你沒有更動產品程式碼)。
  • 它跟其他單元測試應該是完全獨立。
  • 它應該在不需要作業系統檔、網絡、資料庫的情況就能運行。

參考資料

Select a repo