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。
广告