【pytest 教學】#02 撰寫你的第一個測試:基本語法與斷言

測驗:撰寫你的第一個測試:基本語法與斷言

共 5 題,點選答案後會立即顯示結果

1. pytest 如何自動找到測試函式?

  • A. 函式名稱必須以 check_ 開頭
  • B. 函式名稱必須以 test_ 開頭
  • C. 函式必須加上 @test 裝飾器
  • D. 函式必須在 __main__ 區塊中定義

2. 下列哪個檔案命名方式可以被 pytest 自動發現?

  • A. calculator_tests.py
  • B. tests_calculator.py
  • C. calculator_test.py
  • D. check_calculator.py

3. 在 pytest 中,如何測試一個函式是否會拋出 ValueError 例外?

  • A. 使用 assert ValueError
  • B. 使用 try...except 區塊並手動 assert
  • C. 使用 pytest.expect(ValueError)
  • D. 使用 with pytest.raises(ValueError):

4. 執行 pytest 時,測試輸出中的 FAILED 狀態代表什麼意思?

  • A. 測試程式碼本身有語法錯誤
  • B. 斷言失敗,結果不符合預期
  • C. import 模組失敗
  • D. 測試被跳過

5. 下列 pytest 執行指令中,哪一個可以只執行特定的測試函式 test_add

  • A. pytest --only test_add
  • B. pytest -f test_add
  • C. pytest test_calculator.py::test_add
  • D. pytest --run test_add

前言

上一篇我們了解了為什麼需要測試。現在,讓我們動手寫第一個測試。這篇文章會帶你認識 pytest 的基本語法,讓你能夠讀懂並撰寫簡單的測試程式碼。

pytest 如何找到你的測試?

當你執行 pytest 時,它會自動搜尋並執行測試。但它怎麼知道哪些是測試檔案、哪些是測試函式呢?

測試發現規則

pytest 遵循一套簡單的命名規則:

檔案層級:

  • 檔名以 test 開頭,例如 testcalculator.py
  • 或者檔名以 test.py 結尾,例如 calculatortest.py

函式層級:

  • 函式名稱必須以 test 開頭,例如 testadd()

讓我們看一個最小範例:

# test_hello.py

def test_greeting():
    assert "hello" == "hello"
Code language: PHP (php)

這就是一個完整的測試了。檔名是 test_hello.py,函式名是 test_greeting(),兩者都符合命名規則,pytest 就能找到並執行它。

為什麼這樣設計?

這種命名慣例讓你一眼就能區分「正式程式碼」和「測試程式碼」。當你在專案中看到 test_ 開頭的檔案或函式,立刻就知道:「這是測試,不是實際功能。」

assert:測試的核心

pytest 使用 Python 內建的 assert 語句進行斷言。斷言就是「我宣稱這件事是真的」,如果不是真的,測試就會失敗。

基本相等斷言

def test_basic_equality():
    result = 1 + 1
    assert result == 2

這段測試很直白:計算 1 + 1,斷言結果等於 2

常見的斷言模式

def test_various_assertions():
    # 相等
    assert 1 + 1 == 2

    # 不相等
    assert 1 + 1 != 3

    # 大於、小於
    assert 5 > 3
    assert 3 < 5

    # 真假值
    assert True
    assert not False

    # 包含關係
    assert "hello" in "hello world"
    assert 3 in [1, 2, 3]

    # None 檢查
    assert None is None
    result = "something"
    assert result is not None
Code language: PHP (php)

讀懂失敗訊息

當斷言失敗時,pytest 會顯示詳細的錯誤訊息。這是 pytest 最強大的功能之一:

def test_will_fail():
    expected = [1, 2, 3]
    actual = [1, 2, 4]
    assert expected == actual

執行後,你會看到類似這樣的輸出:

FAILED test_example.py::test_will_fail - AssertionError: assert [1, 2, 3] == [1, 2, 4]
Code language: PHP (php)

pytest 會清楚告訴你:

  • 哪個檔案的哪個測試失敗了
  • 預期值和實際值分別是什麼
  • 兩者的差異在哪裡

測試函式的回傳值

實際專案中,我們通常會測試自己寫的函式。以下是一個計算機的範例:

# calculator.py
def add(a, b):
    return a + b

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b
Code language: PHP (php)

對應的測試:

# test_calculator.py
from calculator import add, divide

def test_add_positive_numbers():
    result = add(2, 3)
    assert result == 5

def test_add_negative_numbers():
    result = add(-1, -2)
    assert result == -3

def test_divide():
    result = divide(10, 2)
    assert result == 5.0
Code language: PHP (php)

測試命名技巧

注意測試函式的命名:test_add_positive_numberstest_add_negative_numbers。好的測試名稱會描述「測試什麼情境」,讓你一看就知道這個測試的目的。

測試例外:pytest.raises

有時候,我們預期函式會拋出例外。例如,除以零應該要報錯。pytest 提供了 pytest.raises 來測試這種情況:

import pytest
from calculator import divide

def test_divide_by_zero():
    with pytest.raises(ValueError):
        divide(10, 0)
Code language: JavaScript (javascript)

這段程式碼的意思是:「我預期 divide(10, 0) 會拋出 ValueError。」如果沒有拋出例外,或拋出了其他類型的例外,測試就會失敗。

檢查例外訊息

你還可以進一步檢查例外的訊息內容:

def test_divide_by_zero_message():
    with pytest.raises(ValueError) as exc_info:
        divide(10, 0)

    assert "Cannot divide by zero" in str(exc_info.value)
Code language: JavaScript (javascript)

exc_info.value 包含了被捕捉到的例外物件,你可以檢查它的訊息是否符合預期。

執行測試

基本執行方式

# 執行當前目錄下所有測試
pytest

# 執行特定檔案
pytest test_calculator.py

# 執行特定測試函式
pytest test_calculator.py::test_add_positive_numbers
Code language: PHP (php)

常用選項

# -v:顯示詳細輸出,列出每個測試的名稱和結果
pytest -v

# -q:安靜模式,只顯示摘要
pytest -q

# --tb=short:簡化錯誤訊息
pytest --tb=short
Code language: PHP (php)

解讀測試輸出

執行 pytest -v 後,你會看到類似這樣的輸出:

==================== test session starts ====================
collected 4 items

test_calculator.py::test_add_positive_numbers PASSED
test_calculator.py::test_add_negative_numbers PASSED
test_calculator.py::test_divide PASSED
test_calculator.py::test_divide_by_zero PASSED

==================== 4 passed in 0.02s ====================
Code language: PHP (php)

狀態標記說明

  • PASSED:測試通過,一切正常
  • FAILED:斷言失敗,結果不符合預期
  • ERROR:測試程式碼本身有錯誤(例如 import 失敗)
  • SKIPPED:測試被跳過(通常是故意的)

失敗時的輸出

當測試失敗時,pytest 會顯示詳細的除錯資訊:

==================== FAILURES ====================
__________________ test_will_fail __________________

    def test_will_fail():
        expected = [1, 2, 3]
        actual = [1, 2, 4]
>       assert expected == actual
E       AssertionError: assert [1, 2, 3] == [1, 2, 4]
E         At index 2 diff: 3 != 4

test_example.py:4: AssertionError

這個輸出告訴你:

  1. 哪個測試失敗了(testwillfail
  2. 失敗的那行程式碼(> 標記)
  3. 錯誤類型和詳細比較(E 開頭的行)
  4. 失敗位置(test_example.py:4

組織測試檔案

隨著專案成長,你需要妥善組織測試檔案。一個常見的結構是:

my_project/
├── src/
│   ├── calculator.py
│   └── utils.py
├── tests/
│   ├── test_calculator.py
│   └── test_utils.py
└── pytest.ini

命名對應原則

一個好習慣是讓測試檔案與被測試的模組一一對應:

  • calculator.py 對應 test_calculator.py
  • utils.py 對應 test_utils.py

這樣當你需要找某個模組的測試時,馬上就知道該看哪個檔案。

重點回顧

  1. 命名規則:檔案用 test.pytest.py,函式用 test_*
  2. assert 斷言:直接使用 Python 的 assert 語句
  3. 測試例外:使用 pytest.raises(ExceptionType) 包裝
  4. 執行測試pytestpytest -v 看詳細結果
  5. 解讀輸出:PASSED 表示通過,FAILED 表示失敗

下一步

現在你已經能寫基本的測試了。下一篇,我們會介紹 fixture,這是 pytest 最重要的功能之一,讓你能夠重複使用測試的準備工作,大幅減少重複程式碼。

進階測驗:撰寫你的第一個測試:基本語法與斷言

測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。

1. 你正在為一個字串處理函式撰寫測試,需要驗證當傳入空字串時會拋出 ValueError,且錯誤訊息包含「empty string」。應該怎麼寫? 情境題

  • A. assert process("") raises ValueError
  • B. with pytest.raises(ValueError): process("")
  • C. with pytest.raises(ValueError) as exc_info: process("") 然後 assert "empty string" in str(exc_info.value)
  • D. try: process("") except ValueError as e: assert "empty string" in e

2. 小美執行測試後看到以下輸出,問題最可能出在哪裡? 錯誤診斷

test_utils.py::test_format_date ERROR E ModuleNotFoundError: No module named ‘dateutil’
  • A. 斷言失敗,format_date 函式回傳值不正確
  • B. 測試程式碼本身有錯誤,缺少必要的套件
  • C. 測試函式命名不符合 pytest 規則
  • D. 測試檔案放在錯誤的目錄位置

3. 你的專案有以下結構,想要執行 test_api.py 中的所有測試。應該用什麼指令? 情境題

my_project/ ├── src/ │ └── api.py └── tests/ ├── test_api.py └── test_utils.py
  • A. pytest test_api
  • B. pytest --file test_api.py
  • C. pytest src/test_api.py
  • D. pytest tests/test_api.py

4. 小明寫了一個測試,但執行時 pytest 找不到這個測試。問題在哪裡? 錯誤診斷

# check_calculator.py def verify_addition(): result = add(2, 3) assert result == 5
  • A. 檔名和函式名都不符合 pytest 命名規則
  • B. 只有函式名不符合,檔名是正確的
  • C. 只有檔名不符合,函式名是正確的
  • D. 需要加上 import pytest

5. 你有一個 divide 函式的測試需要同時測試正常情況和例外情況。以下哪種組織方式最符合文章建議的命名原則? 情境題

  • A. 全部寫在同一個 test_divide() 函式中
  • B. 分成 test_divide()test_divide_by_zero() 兩個函式
  • C. 分成 divide_test()divide_error_test() 兩個函式
  • D. 分成 check_divide()check_divide_error() 兩個函式

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *