Pytest 快速指南



Pytest - 簡介

Pytest 是一個基於 Python 的測試框架,用於編寫和執行測試程式碼。在當今 REST 服務盛行的時代,Pytest 主要用於 API 測試,儘管我們可以使用 Pytest 編寫從簡單到複雜的測試,例如,我們可以編寫程式碼來測試 API、資料庫、UI 等。

Pytest 的優點

Pytest 的優點如下:

  • Pytest 可以並行執行多個測試,從而減少測試套件的執行時間。

  • Pytest 有其自身的方法來自動檢測測試檔案和測試函式,如果未明確提及。

  • Pytest 允許我們在執行期間跳過測試的子集。

  • Pytest 允許我們執行整個測試套件的子集。

  • Pytest 是免費且開源的。

  • 由於其簡單的語法,Pytest 非常易於上手。

在本教程中,我們將透過示例程式解釋 Pytest 的基礎知識。

Pytest - 環境設定

在本節中,我們將學習如何安裝 Pytest。

要開始安裝,請執行以下命令:

pip install pytest == 2.9.1

我們可以安裝任何版本的 Pytest。這裡,2.9.1 是我們正在安裝的版本。

要安裝最新版本的 Pytest,請執行以下命令:

pip install pytest

使用以下命令確認安裝,以顯示 Pytest 的幫助部分。

pytest -h

識別測試檔案和測試函式

在不提及檔名的情況下執行 pytest 將運行當前目錄和子目錄中所有格式為 **test_*.py** 或 ***_test.py** 的檔案。Pytest 會自動將這些檔案識別為測試檔案。我們可以透過明確提及它們來讓 Pytest 執行其他檔名。

Pytest 要求測試函式名稱以 **test** 開頭。名稱不為 **test*** 格式的函式名稱不被 Pytest 視為測試函式。我們**不能**顯式地使 Pytest 將任何不以 **test** 開頭的函式視為測試函式。

我們將在後續章節中瞭解測試的執行。

Pytest - 從基本測試開始

現在,我們將從第一個 Pytest 程式開始。我們將首先建立一個目錄,然後在該目錄中建立我們的測試檔案。

讓我們按照以下步驟操作:

  • 建立一個名為 **automation** 的新目錄,並在命令列中導航到該目錄。

  • 建立一個名為 **test_square.py** 的檔案,並將以下程式碼新增到該檔案中。

import math

def test_sqrt():
   num = 25
   assert math.sqrt(num) == 5

def testsquare():
   num = 7
   assert 7*7 == 40

def tesequality():
   assert 10 == 11

使用以下命令執行測試:

pytest

上述命令將生成以下輸出:

test_square.py .F
============================================== FAILURES 
==============================================
______________________________________________ testsquare 
_____________________________________________
   def testsquare():
   num=7
>  assert 7*7 == 40
E  assert (7 * 7) == 40
test_square.py:9: AssertionError
================================= 1 failed, 1 passed in 0.06 seconds 
=================================

請檢視結果的第一行。它顯示檔名和結果。F 代表測試失敗,點 (.) 代表測試成功。

在那之下,我們可以看到失敗測試的詳細資訊。它將顯示測試在哪個語句失敗。在我們的示例中,7*7 與 49 的相等性比較是錯誤的。最後,我們可以看到測試執行摘要,1 個失敗,1 個透過。

函式 tescompare 沒有執行,因為 Pytest 不會將其視為測試,因為其名稱不是 **test*** 格式。

現在,執行以下命令並再次檢視結果:

pytest -v

-v 增加詳細程度。

test_square.py::test_sqrt PASSED
test_square.py::testsquare FAILED
============================================== FAILURES 
==============================================
_____________________________________________ testsquare 
_____________________________________________
   def testsquare():
   num = 7
>  assert 7*7 == 40
E  assert (7 * 7) == 40
test_square.py:9: AssertionError
================================= 1 failed, 1 passed in 0.04 seconds 
=================================

現在,結果更詳細地說明了失敗的測試和透過的測試。

**注意** - pytest 命令將執行當前目錄和子目錄中所有格式為 **test_*** 或 ***_test** 的檔案。

Pytest - 檔案執行

在本節中,我們將學習如何執行單個測試檔案和多個測試檔案。我們已經建立了一個測試檔案 **test_square.py**。建立一個新的測試檔案 **test_compare.py**,其中包含以下程式碼:

def test_greater():
   num = 100
   assert num > 100

def test_greater_equal():
   num = 100
   assert num >= 100

def test_less():
   num = 100
   assert num < 200

現在,要執行所有檔案(此處為 2 個檔案)中的所有測試,我們需要執行以下命令:

pytest -v

上述命令將執行 **test_square.py** 和 **test_compare.py** 中的測試。將生成以下輸出:

test_compare.py::test_greater FAILED
test_compare.py::test_greater_equal PASSED
test_compare.py::test_less PASSED
test_square.py::test_sqrt PASSED
test_square.py::testsquare FAILED
================================================ FAILURES 
================================================
______________________________________________ test_greater 
______________________________________________
   def test_greater():
   num = 100
>  assert num > 100
E  assert 100 > 100

test_compare.py:3: AssertionError
_______________________________________________ testsquare 
_______________________________________________
   def testsquare():
   num = 7
>  assert 7*7 == 40
E  assert (7 * 7) == 40

test_square.py:9: AssertionError
=================================== 2 failed, 3 passed in 0.07 seconds 
===================================

要執行特定檔案中的測試,請使用以下語法:

pytest <filename> -v

現在,執行以下命令:

pytest test_compare.py -v

上述命令將僅執行 **test_compare.py** 檔案中的測試。我們的結果將是:

test_compare.py::test_greater FAILED
test_compare.py::test_greater_equal PASSED
test_compare.py::test_less PASSED
============================================== FAILURES 
==============================================
____________________________________________ test_greater 
____________________________________________
   def test_greater():
   num = 100
>  assert num > 100
E  assert 100 > 100
test_compare.py:3: AssertionError
================================= 1 failed, 2 passed in 0.04 seconds 
=================================

執行測試套件的子集

在實際場景中,我們將有多個測試檔案,每個檔案將包含多個測試。測試將涵蓋各種模組和功能。假設我們只想執行一組特定的測試;我們該如何操作呢?

Pytest 提供兩種方法來執行測試套件的子集。

  • 根據測試名稱的子字串匹配選擇要執行的測試。
  • 根據應用的標記選擇要執行的測試組。

我們將在後續章節中透過示例解釋這兩點。

測試名稱的子字串匹配

要執行測試名稱中包含字串的測試,我們可以使用以下語法:

pytest -k <substring> -v

-k <substring> 表示要在測試名稱中搜索的子字串。

現在,執行以下命令:

pytest -k great -v

這將執行測試名稱中包含單詞 **‘great’** 的所有測試。在這種情況下,它們是 **test_greater()** 和 **test_greater_equal()**。請檢視下面的結果。

test_compare.py::test_greater FAILED
test_compare.py::test_greater_equal PASSED
============================================== FAILURES 
==============================================
____________________________________________ test_greater 
____________________________________________
def test_greater():
num = 100
>  assert num > 100
E  assert 100 > 100
test_compare.py:3: AssertionError
========================== 1 failed, 1 passed, 3 deselected in 0.07 seconds 
==========================

在這裡的結果中,我們可以看到 3 個測試被取消選擇。這是因為這些測試名稱中不包含單詞 **great**。

**注意** - 測試函式的名稱仍然應該以 'test' 開頭。

Pytest - 測試分組

在本節中,我們將學習如何使用標記對測試進行分組。

Pytest 允許我們在測試函式上使用標記。標記用於為測試函式設定各種特性/屬性。Pytest 提供了許多內建標記,例如 xfail、skip 和 parametrize。除此之外,使用者還可以建立自己的標記名稱。使用下面給出的語法將標記應用於測試:

@pytest.mark.<markername>

要使用標記,我們必須在測試檔案中 **匯入 pytest** 模組。我們可以將我們自己的標記名稱定義到測試中,並執行具有這些標記名稱的測試。

要執行標記的測試,我們可以使用以下語法:

pytest -m <markername> -v

-m <markername> 表示要執行的測試的標記名稱。

使用以下程式碼更新我們的測試檔案 **test_compare.py** 和 **test_square.py**。我們定義了 3 個標記 **– great、square、others**。

test_compare.py

import pytest
@pytest.mark.great
def test_greater():
   num = 100
   assert num > 100

@pytest.mark.great
def test_greater_equal():
   num = 100
   assert num >= 100

@pytest.mark.others
def test_less():
   num = 100
   assert num < 200

test_square.py

import pytest
import math

@pytest.mark.square
def test_sqrt():
   num = 25
   assert math.sqrt(num) == 5

@pytest.mark.square
def testsquare():
   num = 7
   assert 7*7 == 40

@pytest.mark.others
   def test_equality():
   assert 10 == 11

現在,要執行標記為 **others** 的測試,請執行以下命令:

pytest -m others -v

請檢視下面的結果。它運行了標記為 **others** 的 2 個測試。

test_compare.py::test_less PASSED
test_square.py::test_equality FAILED
============================================== FAILURES
==============================================
___________________________________________ test_equality
____________________________________________
   @pytest.mark.others
   def test_equality():
>  assert 10 == 11
E  assert 10 == 11
test_square.py:16: AssertionError
========================== 1 failed, 1 passed, 4 deselected in 0.08 seconds
==========================

同樣,我們也可以執行具有其他標記的測試 - great、compare

Pytest - Fixture

Fixture 是函式,它將在應用於其的每個測試函式之前執行。Fixture 用於向測試提供一些資料,例如資料庫連線、要測試的 URL 和某種輸入資料。因此,與其為每個測試執行相同的程式碼,我們可以將 fixture 函式附加到測試中,它將在執行每個測試之前執行並向測試返回資料。

一個函式透過以下方式標記為 fixture:

@pytest.fixture

測試函式可以透過將 fixture 名稱作為輸入引數來使用 fixture。

建立一個檔案 **test_div_by_3_6.py** 並將以下程式碼新增到其中

import pytest

@pytest.fixture
def input_value():
   input = 39
   return input

def test_divisible_by_3(input_value):
   assert input_value % 3 == 0

def test_divisible_by_6(input_value):
   assert input_value % 6 == 0

這裡,我們有一個名為 **input_value** 的 fixture 函式,它向測試提供輸入。要訪問 fixture 函式,測試必須將 fixture 名稱作為輸入引數提及。

Pytest 在測試執行期間,將看到 fixture 名稱作為輸入引數。然後它執行 fixture 函式,並將返回值儲存到輸入引數中,測試可以使用該引數。

使用以下命令執行測試:

pytest -k divisible -v

上述命令將生成以下結果:

test_div_by_3_6.py::test_divisible_by_3 PASSED
test_div_by_3_6.py::test_divisible_by_6 FAILED
============================================== FAILURES
==============================================
________________________________________ test_divisible_by_6
_________________________________________
input_value = 39
   def test_divisible_by_6(input_value):
>  assert input_value % 6 == 0
E  assert (39 % 6) == 0
test_div_by_3_6.py:12: AssertionError
========================== 1 failed, 1 passed, 6 deselected in 0.07 seconds
==========================

但是,這種方法有其自身的侷限性。在測試檔案中定義的 fixture 函式的範圍僅限於測試檔案內。我們不能在另一個測試檔案中使用該 fixture。要使 fixture 可用於多個測試檔案,我們必須在名為 conftest.py 的檔案中定義 fixture 函式。**conftest.py** 在下一節中解釋。

Pytest - conftest.py

我們可以在此檔案中定義 fixture 函式,以使其可在多個測試檔案中訪問。

建立一個新檔案 **conftest.py** 並將以下程式碼新增到其中:

import pytest

@pytest.fixture
def input_value():
   input = 39
   return input

編輯 **test_div_by_3_6.py** 以刪除 fixture 函式:

import pytest

def test_divisible_by_3(input_value):
   assert input_value % 3 == 0

def test_divisible_by_6(input_value):
   assert input_value % 6 == 0

建立一個新檔案 **test_div_by_13.py**:

import pytest

def test_divisible_by_13(input_value):
   assert input_value % 13 == 0

現在,我們有 **test_div_by_3_6.py** 和 **test_div_by_13.py** 檔案使用在 **conftest.py** 中定義的 fixture。

透過執行以下命令來執行測試:

pytest -k divisible -v

上述命令將生成以下結果:

test_div_by_13.py::test_divisible_by_13 PASSED
test_div_by_3_6.py::test_divisible_by_3 PASSED
test_div_by_3_6.py::test_divisible_by_6 FAILED
============================================== FAILURES
==============================================
________________________________________ test_divisible_by_6
_________________________________________
input_value = 39
   def test_divisible_by_6(input_value):
>  assert input_value % 6 == 0
E  assert (39 % 6) == 0
test_div_by_3_6.py:7: AssertionError
========================== 1 failed, 2 passed, 6 deselected in 0.09 seconds
==========================

測試將查詢同一檔案中的 fixture。由於在檔案中找不到 fixture,它將檢查 conftest.py 檔案中的 fixture。找到後,將呼叫 fixture 方法,並將結果返回到測試的輸入引數。

Pytest - 引數化測試

測試的引數化是為多個輸入集執行測試。我們可以使用以下標記來做到這一點:

@pytest.mark.parametrize

將以下程式碼複製到名為 **test_multiplication.py** 的檔案中:

import pytest

@pytest.mark.parametrize("num, output",[(1,11),(2,22),(3,35),(4,44)])
def test_multiplication_11(num, output):
   assert 11*num == output

這裡,測試將輸入乘以 11 並將結果與預期輸出進行比較。測試有 4 組輸入,每組有兩個值 - 一個是要乘以 11 的數字,另一個是預期結果。

透過執行以下命令來執行測試:

Pytest -k multiplication -v

上述命令將生成以下輸出:

test_multiplication.py::test_multiplication_11[1-11] PASSED
test_multiplication.py::test_multiplication_11[2-22] PASSED
test_multiplication.py::test_multiplication_11[3-35] FAILED
test_multiplication.py::test_multiplication_11[4-44] PASSED
============================================== FAILURES
==============================================
_________________ test_multiplication_11[3-35] __________________
num = 3, output = 35
   @pytest.mark.parametrize("num, output",[(1,11),(2,22),(3,35),(4,44)])
   def test_multiplication_11(num, output):
>  assert 11*num == output
E  assert (11 * 3) == 35
test_multiplication.py:5: AssertionError
============================== 1 failed, 3 passed, 8 deselected in 0.08 seconds
==============================

Pytest - Xfail/Skip 測試

在本節中,我們將學習 Pytest 中的 Skip 和 Xfail 測試。

現在,考慮以下情況:

  • 由於某些原因,測試在一段時間內不相關。
  • 正在實施一項新功能,我們已經為此功能添加了一個測試。

在這些情況下,我們可以選擇 xfail 測試或跳過測試。

Pytest 將執行 xfailed 測試,但它不會被視為失敗或透過的測試的一部分。即使測試失敗,這些測試的詳細資訊也不會列印(記住 pytest 通常會列印失敗測試的詳細資訊)。我們可以使用以下標記來 xfail 測試:

@pytest.mark.xfail

跳過測試意味著不會執行測試。我們可以使用以下標記來跳過測試:

@pytest.mark.skip

稍後,當測試變得相關時,我們可以刪除標記。

編輯我們已經擁有的 **test_compare.py** 以包含 xfail 和 skip 標記:

import pytest
@pytest.mark.xfail
@pytest.mark.great
def test_greater():
   num = 100
   assert num > 100

@pytest.mark.xfail
@pytest.mark.great
def test_greater_equal():
   num = 100
   assert num >= 100

@pytest.mark.skip
@pytest.mark.others
def test_less():
   num = 100
   assert num < 200

使用以下命令執行測試:

pytest test_compare.py -v

執行後,上述命令將生成以下結果:

test_compare.py::test_greater xfail
test_compare.py::test_greater_equal XPASS
test_compare.py::test_less SKIPPED
============================ 1 skipped, 1 xfailed, 1 xpassed in 0.06 seconds
============================

Pytest - 在 N 次測試失敗後停止測試套件

在實際場景中,一旦準備就緒要部署新版本的程式碼,它首先會部署到預生產/登臺環境中。然後,測試套件會在其上執行。

只有當測試套件全部透過時,程式碼才符合生產部署的條件。如果出現測試失敗,無論是一次還是多次失敗,程式碼都無法用於生產。

因此,如果我們想在n個測試失敗後立即停止測試套件的執行,該怎麼辦呢?這可以使用pytest中的`maxfail`選項來實現。

在n個測試失敗後立即停止測試套件執行的語法如下:

pytest --maxfail = <num>

建立一個名為`test_failure.py`的檔案,其中包含以下程式碼。

import pytest
import math

def test_sqrt_failure():
   num = 25
   assert math.sqrt(num) == 6

def test_square_failure():
   num = 7
   assert 7*7 == 40

def test_equality_failure():
   assert 10 == 11

執行此測試檔案將導致所有3個測試失敗。在這裡,我們將透過以下方式在第一次失敗後停止測試執行:

pytest test_failure.py -v --maxfail 1
test_failure.py::test_sqrt_failure FAILED
=================================== FAILURES
=================================== _______________________________________
test_sqrt_failure __________________________________________
   def test_sqrt_failure():
   num = 25
>  assert math.sqrt(num) == 6
E  assert 5.0 == 6
E  + where 5.0 = <built-in function sqrt>(25)
E  + where <built-in function sqrt>= math.sqrt
test_failure.py:6: AssertionError
=============================== 1 failed in 0.04 seconds
===============================

從上面的結果可以看出,執行在第一次失敗時停止了。

Pytest - 並行執行測試

預設情況下,pytest按順序執行測試。在實際場景中,一個測試套件將包含許多測試檔案,每個檔案將包含許多測試。這將導致較長的執行時間。為了克服這個問題,pytest提供了一個並行執行測試的選項。

為此,我們需要首先安裝pytest-xdist外掛。

執行以下命令安裝pytest-xdist:

pip install pytest-xdist

現在,我們可以使用語法`pytest -n `執行測試。

pytest -n 3

`-n ` 使用多個工作程序執行測試,這裡為3。

如果只有少量測試需要執行,時間差異不會很大。但是,當測試套件很大時,這將非常重要。

XML格式的測試執行結果

我們可以生成測試執行的詳細資訊到一個xml檔案中。這個xml檔案主要用於那些具有顯示測試結果的儀表盤的場景。在這種情況下,可以解析xml以獲取執行的詳細資訊。

我們現在將執行`test_multiplcation.py`中的測試,並透過執行以下命令生成xml:

pytest test_multiplication.py -v --junitxml="result.xml"

現在我們可以看到生成了包含以下資料的`result.xml`:

<?xml version = "1.0" encoding = "utf-8"?>
<testsuite errors = "0" failures = "1"
name = "pytest" skips = "0" tests = "4" time = "0.061">
   <testcase classname = "test_multiplication"          
      file = "test_multiplication.py"
      line = "2" name = "test_multiplication_11[1-11]"
      time = "0.00117516517639>
   </testcase>
   
   <testcase classname = "test_multiplication"    
      file = "test_multiplication.py"
      line = "2" name = "test_multiplication_11[2-22]"
      time = "0.00155973434448">
   </testcase>

   <testcase classname = "test_multiplication" 
      file = "test_multiplication.py"
      line = "2" name = "test_multiplication_11[3-35]" time = "0.00144290924072">
      failure message = "assert (11 * 3) == 35">num = 3, output = 35

         @pytest.mark.parametrize("num,
         output",[(1,11),(2,22),(3,35),(4,44)])
            
         def test_multiplication_11(num, output):> 
         assert 11*num == output
         E assert (11 * 3) == 35

         test_multiplication.py:5: AssertionErro
      </failure>
   </testcase>
   <testcase classname = "test_multiplication" 
      file = "test_multiplication.py"
      line = "2" name = "test_multiplication_11[4-44]"
      time = "0.000945091247559">
   </testcase>
</testsuite>

這裡,標籤``總結了共有4個測試,失敗數量為1。

  • 標籤``提供了每個已執行測試的詳細資訊。

  • <failure> 標籤提供了失敗測試程式碼的詳細資訊。

Pytest - 總結

在本pytest教程中,我們涵蓋了以下方面:

  • 安裝pytest。
  • 識別測試檔案和測試函式。
  • 使用`pytest –v`執行所有測試檔案。
  • 使用`pytest -v`執行特定檔案。
  • 使用子字串匹配執行測試:`pytest -k -v`。
  • 基於標記執行測試:`pytest -m -v`。
  • 使用`@pytest.fixture`建立fixture。
  • `conftest.py`允許從多個檔案中訪問fixture。
  • 使用`@pytest.mark.parametrize`引數化測試。
  • 使用`@pytest.mark.xfail`使測試預期失敗。
  • 使用`@pytest.mark.skip`跳過測試。
  • 使用`pytest --maxfail = `在n次失敗後停止測試執行。
  • 使用`pytest -n `並行執行測試。
  • 使用`pytest -v --junitxml = "result.xml"`生成結果xml。

Pytest - 結論

本教程介紹了pytest框架。現在您應該能夠開始使用pytest編寫測試了。

作為一個好的實踐:

  • 根據被測試的功能/模組建立不同的測試檔案。
  • 為測試檔案和方法起有意義的名字。
  • 使用足夠的標記根據各種標準對測試進行分組。
  • 在需要時使用fixture。
廣告
© . All rights reserved.