Python 单元测试

2025年3月17日 | 阅读11分钟

在本教程中,我们将使用 Python 实现单元测试。Python 中的单元测试本身是一个庞大的话题,但我们将涵盖一些基本概念。

什么是 Python unittest?

单元测试是一种技术,其中由开发人员亲自测试特定模块,以检查是否存在任何错误。单元测试的主要重点是测试系统的个体单元,以分析、检测和修复错误。

Python 提供了 unittest 模块来测试源代码的单元。当我们编写大量代码时,unittest 发挥着至关重要的作用,它提供了检查输出是否正确的便利功能。

通常,我们会打印值并将其与参考输出进行匹配,或者手动检查输出。

这个过程非常耗时。为了解决这个问题,Python 推出了 unittest 模块。我们还可以使用它来检查应用程序的性能。

我们将学习如何创建基本的测试、查找 bug,并在代码交付给用户之前执行它们。

测试代码

我们可以通过多种方式测试我们的代码。在本节中,我们将学习从基本步骤到高级方法的知识。

自动化测试与手动测试

手动测试还有另一种形式,称为探索性测试。这是一种无需任何计划即可进行的测试。要进行手动测试,我们需要准备应用程序的列表;我们输入不同的输入并等待预期的输出。

每次我们输入或更改代码时,都需要检查列表中的每一项功能。

这是最常见的测试方式,也是一个非常耗时的过程。

另一方面,自动化测试是根据我们的代码计划执行代码,这意味着它会通过脚本而不是人工运行我们想要测试的代码部分,并按照我们想要测试的顺序运行。

Python 提供了一套工具和库,帮助我们为应用程序创建自动化测试。

单元测试与集成测试

假设我们想检查汽车的车灯,我们可能会如何测试它们?我们会打开车灯,走到车外,或者让朋友帮忙看看车灯是否亮了。“打开车灯”将被视为测试步骤,“走到车外或请朋友帮忙”则被称为测试断言。集成测试中,我们可以同时测试多个组件。

这些组件可以是代码中的任何内容,例如我们编写的函数、类和模块。

但是集成测试有一个局限性:如果集成测试没有给出预期的结果,该怎么办?在这种情况下,很难确定系统的哪个部分出现了问题。让我们以前面的例子为例;如果车灯没有亮,可能是电池没电了,灯泡坏了,汽车的电脑出了故障。

这就是为什么我们考虑进行单元测试以了解被测代码的确切问题。

单元测试是一个较小的测试,它检查单个组件是否正常工作。通过单元测试,我们可以分离出系统中需要修复的部分。

到目前为止,我们已经看到了两种测试类型;集成测试检查多个组件;而单元测试检查应用程序中的小组件。

让我们理解下面的例子。

我们将内置的 Python 内置函数 sum() 应用于单元测试,并与已知输出进行比较。我们检查数字 (2, 3, 5) 的 sum() 是否等于 10。

上面的行将返回正确的结果,因为值是正确的。如果我们传递错误的参数,它将返回 Assertion error。例如 -

我们可以将上面的代码放入文件中,并在命令行中再次执行。

输出

$ python sum.py
Everything is correct

在下面的示例中,我们将传递一个元组进行测试。创建一个名为 test_sum2.py 的新文件。

示例 - 2

输出

Everything is correct
Traceback (most recent call last):
  File "<string>", line 13, in <module>
File "<string>", line 9, in test_sum_tuple
AssertionError: It should be 10

解释 -

在上面的代码中,我们向 test_sum_tuple() 传递了错误的输入。输出与预期结果不符。

上面的方法很好,但是如果存在多个错误怎么办?如果遇到第一个错误,Python 解释器会立即报错。为了解决这个问题,我们使用测试运行器。

测试运行器是专门为测试输出、运行测试以及提供用于修复和诊断测试和应用程序的工具而设计的应用程序。

选择测试运行器

Python 包含许多测试运行器。最受欢迎的内置 Python 库是 unittest。unittest 可以移植到其他框架。考虑以下三个顶级测试运行器。

  • unittest
  • nose 或 nose2
  • pytest

我们可以根据自己的需求选择其中任何一个。让我们来做一个简要介绍。

unittest

unittest 自 2.1 版本以来就内置于 Python 标准库中。unittest 最棒的一点是,它同时提供了测试框架和测试运行器。编写和执行代码需要满足 unittest 的一些要求。

  • 代码必须使用类和函数编写。
  • 除了内置的 assert 语句外,TestCase 类中还有一系列不同的断言方法。

让我们使用 unittest case 来实现上面的示例。

示例 -

输出

.F
-
FAIL: test_sum_tuple (__main__.TestingSum)
--
Traceback (most recent call last):
  File "<string>", line 11, in test_sum_tuple
AssertionError: 9 != 10 : It should be 10

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)
Traceback (most recent call last):
  File "<string>", line 14, in <module>
  File "/usr/lib/python3.8/unittest/main.py", line 101, in __init__
    self.runTests()
  File "/usr/lib/python3.8/unittest/main.py", line 273, in runTests
    sys.exit(not self.result.wasSuccessful())
SystemExit: True

正如我们在输出中看到的,它用 点 (.) 表示成功执行,用 F 表示一次失败。

nose

有时,我们需要为应用程序编写成百上千行测试代码,这会变得非常难以理解。

nose 测试运行器可以很好地替代 unittest 测试运行器,因为它与使用 unittest 框架编写的任何测试都兼容。nose 有两种类型——nose 和 nose2。我们建议使用 nose2,因为它是最新版本。

要使用 nose2,我们需要使用以下命令进行安装。

在终端中运行以下命令,使用 nose2 测试代码。

输出如下。

FAIL: test_sum_tuple (__main__.TestSum)
--
Traceback (most recent call last):
  File "test_sum_unittest.py", line 10, in test_sum_tuple
    self.assertEqual(sum((2, 3, 5)), 10, "It should be 10")
AssertionError: It should be 10

--
Ran 2 tests in 0.001s

FAILED (failures=1)

nose2 提供了许多用于过滤测试的命令行标志。您可以在其官方文档中了解更多信息。

pytest

pytest 测试运行器支持执行 unittest 测试用例。pytest 的真正好处在于编写 pytest 测试用例。pytest 测试用例通常是 Python 文件中以 . 开头的方法序列。

pytest 提供了以下好处 -

  • 它支持内置的 assert 语句,而不是使用特殊的 assert*() 方法。
  • 它还为测试用例提供了清理支持。
  • 它可以从最后一个用例重新运行。
  • 它拥有一个由数百个插件组成的生态系统,可以扩展其功能。

让我们理解下面的例子。

示例 -

编写第一个测试

在这里,我们将应用之前学到的所有概念。首先,我们需要创建一个名为 test.py 的文件,或者任何其他名称。然后输入数据并执行被测代码,捕获输出。代码成功运行后,将输出与预期结果进行匹配。

首先,我们创建 my_sum 文件并在其中编写代码。

我们初始化了 total 变量,该变量会遍历 arg 中的所有值。

现在,我们创建一个名为 test.py 的文件,其中包含以下代码。

示例 -

输出

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

说明

在上面的代码中,我们从创建的 my_sum 包中导入了 sum()。我们定义了 Checkclass,它继承自 unittest.TestCase。有一个测试方法——.test_list_int(),用于测试整数。

运行代码后,它返回 点 (.),表示代码没有错误。

让我们来看另一个例子。

示例 - 2

输出

Peter Decosta has been added with id 0
The user associated with id 0 is Peter

Python 基本函数和单元测试输出

unittest 模块会产生三种可能的输出。以下是可能的输出。

  1. OK - 如果所有测试都通过,它将返回 OK。
  2. Failure - 如果任何测试失败,它将引发 AssertionError 异常。
  3. Error - 如果发生任何错误而不是 Assertion error。

让我们看看以下基本函数。

方法描述
.assertEqual(a, b)a == b
.assertTrue(x)bool(x) is True
.assertFalse(x)bool(x) is False
.assertIs(a, b)a is b
.assertIsNone(x)x is None
.assertIn(a, b)a in b
.assertIsInstance(a, b)isinstance(a, b)
.assertNotIn(a, b)a not in b
.assertNotIsInstance(a,b)not isinstance(a, b)
.assertIsNot(a, b)a is not b

Python 单元测试示例

输出

Start set_name test

The length of user_id is =  4
[0, 1, 2, 3]
The length of user_name is =  4
['name0', 'name1', 'name2', 'name3']

Finish set_name test


Start get_name test

The length of user_id is =  4
The lenght of user_name is =  4
Testing for get_name no user test
.F
======================================================================
FAIL: test_1_get_name (__main__.Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:/Users/DEVANSH SHARMA/PycharmProjects/Hello/multiprocessing.py", line 502, in test_1_get_name
    self.assertEqual('There is no such user', self.person.get_name(i))
AssertionError: 'There is no such user' != ' No such user Find'
- There is no such user
+  No such user Find


----------------------------------------------------------------------
Ran 2 tests in 0.002s

FAILED (failures=1)

高级测试场景

在为应用程序创建测试时,我们必须遵循给定的步骤。

  • 生成必要的输入
  • 执行代码,获取输出。
  • 将输出与预期结果进行匹配。

创建静态值(如字符串或数字)等输入值是一项相当复杂的任务。有时,我们需要创建类实例或上下文。

我们创建的输入数据称为“fixture”(测试夹具)。我们可以在应用程序中重用 fixture。

当我们反复运行代码并每次传入不同的值并期望得到相同的结果时,这个过程称为参数化

处理预期失败

在前面的示例中,我们传入了整数来测试 sum();如果我们传入了错误的值,例如单个整数或字符串,会发生什么?

sum() 将如预期一样抛出错误。这将是由于测试失败。

我们可以使用 .assertRaises() 来处理预期的错误。它在 with 语句中使用。让我们理解下面的示例。

示例 -

输出

..
----------------------------------------------------------------------
Ran 2 tests in 0.006s

OK

Python 单元测试跳过测试

我们可以使用 skip test 技术跳过单个测试方法或 TestCase。失败将不会在 TestResult 中计为失败。

考虑以下示例,无条件跳过该方法。

示例 -

输出

s
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK (skipped=1)

说明

在上面的示例中,skip() 方法以 @token 为前缀。它接受一个参数——一个日志消息,我们可以在其中描述跳过的原因。s 字符表示测试已成功跳过。

我们可以基于特定条件跳过某个方法或代码块。

示例 - 2

输出

Fsx.
======================================================================
FAIL: test_add (__main__.suiteTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:/Users/DEVANSH SHARMA/PycharmProjects/Hello/multiprocessing.py", line 539, in test_add
    self.assertEqual(res, 100)
AssertionError: 50 != 100

----------------------------------------------------------------------
Ran 4 tests in 0.001s

FAILED (failures=1, skipped=1, expected failures=1)

说明

正如我们在输出中看到的,条件 b == 0 和 a>b 为真,因此 test_mul() 方法被跳过。另一方面,test_mul 被标记为预期失败。

结论

我们已经讨论了与 Python 单元测试相关的所有重要概念。作为初学者,我们需要编写智能、可维护的方法来验证我们的代码。一旦我们对 Python 单元测试有了相当的掌握,我们就可以转向其他框架,例如 pytest,并利用更高级的功能。