跳至主要内容

Python 单元测试教程

了解什么是单元测试、它为何重要,以及如何借助 Python 实施。
· 10分钟 阅读

使用 AI 探索

单元测试是一种软件测试方法,通过对源代码的各个独立单元进行多种测试,判断其是否可用,从而评估并确保代码质量。

通常在开发过程完成后,开发者会将已知可行且有用的判定标准或期望结果编码进测试脚本,以验证特定单元的正确性。在执行测试用例时,各类框架会记录未通过任一标准的测试,并在摘要中报告。

开发者应编写自动化测试脚本,以确保每个部分或单元都符合其设计并按预期运行。

Python 单元测试

尽管为代码编写手动测试既繁琐又耗时,但 Python 内置的单元测试框架让这一切轻松了许多。

Python 的单元测试框架称为 unittest,随 Python 一同提供。

单元测试让您的代码更具前瞻性,因为您会预先考虑到代码可能失败或产生缺陷的情形。虽然不可能预测所有情况,但仍能覆盖大多数。

一个单元可以划分为多种类别:

  • 整个模块,
  • 某个独立函数,
  • 一个完整接口,如类或方法。

编写单元测试的最佳方式是,从代码中最小的可测试单元开始,然后再转向其他单元,观察该最小单元如何与其他单元交互。这样,您就能逐步为应用构建起一套全面的单元测试。

Python 的单元测试框架受 Java 的 JUnit 启发,具备与其他语言主流单元测试框架类似的特性。Python 的单元测试框架提供多种 功能,包括:

  • 测试自动化
  • 测试的初始化与清理代码复用
  • 将测试聚合为集合
  • 测试与报告框架解耦

现在让我们通过一个示例来理解为什么需要为代码编写单元测试。

开始使用 Python unittest

下面概述了使用 Python unittest 框架所需的步骤

创建一个立方体函数

我们来编写一段 Python 代码,用于计算正方体的体积

def cuboid_volume(l):
    return (l*l*l)
length = [2,1.1, -2.5, 2j, 'two']
for i in range(len(length)):
    print ("The volume of cuboid:",cuboid_volume(length[i]))
The volume of cuboid: 8
The volume of cuboid: 1.3310000000000004
The volume of cuboid: -15.625
The volume of cuboid: (-0-8j)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-7-67e1c37a435f> in <module>
      1 for i in range(len(length)):
----> 2     print ("The volume of cuboid:",cuboid_volume(length[i]))
<ipython-input-2-f50464fd88da> in cuboid_volume(l)
      1 def cuboid_volume(l):
----> 2     return (l*l*l)
TypeError: can't multiply sequence by non-int of type 'str'

结果分析

上面的输出能帮助您直观理解为何需要为代码编写单元测试。上述代码中有三处明显不正确:

  • 其一,正方体体积为负数,
  • 其二,正方体体积为复数,
  • 最后,代码导致 TypeError,因为无法将字符串(非 int)进行乘法。

第三个问题还好触发了错误,而前两个虽然不合逻辑(体积不可能为负或复数),却仍然“成功”了。

单元测试通常作为独立代码写在不同文件中,命名也有多种约定。您可以使用 代码/单元名 + test(下划线分隔),或 test + 代码/单元名(下划线分隔)。

例如,如果上述代码文件名为 cuboid_volume.py,那么您的单元测试文件名可以是 cuboid_volume_test.py

事不宜迟,我们来为上面的代码编写单元测试。

使用 assertAlmostEqual

首先,新建名为 volume_cuboid.py 的 Python 文件,编写计算体积的代码;再新建名为 test_volume_cuboid.py 的文件,编写单元测试代码。

导入 unittest

类 TestCuboid 继承自 unittest 模块中的基类,在该类中定义若干方法,用于核对您的函数 cuboid_volume 的行为。

首先定义函数 test_volume,用于检查 cuboid_volume 的输出是否符合预期。为此将使用 assertAlmostEqual 方法。

测试 1

class TestCuboid(unittest.TestCase):
    def test_volume(self):
        self.assertAlmostEqual(cuboid_volume(2),8)
        self.assertAlmostEqual(cuboid_volume(1),1)
        self.assertAlmostEqual(cuboid_volume(0),0)
        self.assertAlmostEqual(cuboid_volume(5.5),166.375)

代码 1

代码 2

我们来运行上述脚本。您将通过指定 -m 参数,将 unittest 模块作为脚本来运行。

!python -m unittest test_volume_cuboid.py

输出:

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

太好了!您的第一个单元测试代码已成功运行。

测试成功并返回 OK,这意味着 cuboid_volume 函数按预期工作。

测试 2

我们看看当某个 assertAlmostEqual 断言失败时会发生什么。

代码 3

请注意,最后一条断言语句已被修改。

!python -m unittest test_volume_cuboid.py

输出:

F
======================================================================
FAIL: test_volume (test_volume_cuboid.TestCuboid)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\hda3kor\Documents\Unit_Testing_Python\test_volume_cuboid.py", line 15, in test_volume
    self.assertAlmostEqual(cuboid_volume(5.5),0)
AssertionError: 166.375 != 0 within 7 places (166.375 difference)
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)

从以上输出可以看出,最后一条断言语句触发了 AssertionError,因此单元测试失败。Python 的单元测试模块会显示失败原因及失败用例数量。

使用 assertRaises

接下来我们看看另一个断言方法 assertRaises,它能帮助您判断函数 cuboid_volume 是否正确处理输入值。

比如,您想测试函数 cuboid_volume 是否会检查输入的类型:当传入字符串时,它是否会通过抛出异常或 if 条件来处理(因为正方体的边长不可能是字符串)。

代码 4

!python -m unittest test_volume_cuboid.py

输出:

F.
======================================================================
FAIL: test_input_value (test_volume_cuboid.TestCuboid)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\hda3kor\Documents\Unit_Testing_Python\test_volume_cuboid.py", line 17, in test_input_value
    self.assertRaises(TypeError, cuboid_volume, True)
AssertionError: TypeError not raised by cuboid_volume
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)

很好!从上述输出可以看出,您的 volume_cuboid.py 并未正确处理传入的输入。

修改立方体函数

让我们在 volume_cuboid.py 中增加条件判断,检查传入的正方体边长是否为布尔或字符串,并抛出错误。

代码 5

!python -m unittest test_volume_cuboid.py

输出:

..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK

在 JupyterLab 中运行 Python 测试脚本

现在在 JupyterLab 中运行测试脚本。首先在 JupyterLab 中定义测试脚本,如下所示。

# -*- coding: utf-8 -*-
"""
Created on Sat Apr 25 20:16:58 2020
@author: Aditya
"""
from volume_cuboid import *
import unittest
class TestCuboid(unittest.TestCase):
    def test_volume(self):
        self.assertAlmostEqual(cuboid_volume(2),8)
        self.assertAlmostEqual(cuboid_volume(1),1)
        self.assertAlmostEqual(cuboid_volume(0),1)
    def test_input_value(self):
        self.assertRaises(TypeError, cuboid_volume, True)

然后使用 unittest.main() 方法运行测试脚本。该方法可接收多个参数,其中之一是 verbosity level(详细程度)。

我们来试验不同的详细程度,看看输出说明如何变化。

详细程度 0

unittest.main(argv=[''],verbosity=0, exit=False)
Output:
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
<unittest.main.TestProgram at 0x1de02774348>

详细程度 1

unittest.main(argv=[''],verbosity=1, exit=False)
Output:
..
----------------------------------------------------------------------
Ran 2 tests in 0.002s
OK
<unittest.main.TestProgram at 0x1de027a1cc8>

详细程度 2

unittest.main(argv=[''],verbosity=2, exit=False)
Output:
test_input_value (__main__.TestCuboid) ... ok
test_volume (__main__.TestCuboid) ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.002s
OK
<unittest.main.TestProgram at 0x1de027a8308>

可视化 AssertionError

最后,我们修改 test_volume 方法中的最后一个 assertAlmostEqual,并观察详细程度为 2 时如何展示方法失败。

# -*- coding: utf-8 -*-
"""
Created on Sat Apr 25 20:16:58 2020
@author: Aditya
"""
from volume_cuboid import *
import unittest
class TestCuboid(unittest.TestCase):
    def test_volume(self):
        self.assertAlmostEqual(cuboid_volume(2),8)
        self.assertAlmostEqual(cuboid_volume(1),1)
        self.assertAlmostEqual(cuboid_volume(0),1)
    def test_input_value(self):
        self.assertRaises(TypeError, cuboid_volume, True)
unittest.main(argv=[''],verbosity=2, exit=False)

输出:

test_input_value (__main__.TestCuboid) ... ok
test_volume (__main__.TestCuboid) ... FAIL
======================================================================
FAIL: test_volume (__main__.TestCuboid)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-28-46ad33b909ee>", line 14, in test_volume
    self.assertAlmostEqual(cuboid_volume(0),1)
AssertionError: 0 != 1 within 7 places (1 difference)
----------------------------------------------------------------------
Ran 2 tests in 0.003s
FAILED (failures=1)
<unittest.main.TestProgram at 0x1de027b5448>

小结

根据上述输出,可以得出以下结论:

  • 详细程度 0 仅显示运行了多少测试(如 2 个)以及耗时,
  • 详细程度 1 会增加两个点 ..,表示执行了两个测试,
  • 详细程度 2 会显示运行的具体方法名以及其状态为 OK 或 FAIL。

可用的 unittest 断言方法

Python 的单元测试模块中有许多断言方法,可用于您的测试场景。

常用断言方法

常用断言方法

来源: unittest 文档

 异常/警告/日志检查

使用以下方法,您可以检查异常、警告和日志消息的产生。

异常/警告/日志检查

来源: unittest 文档

 任务特定方法

任务特定方法

来源: unittest 文档

 

如需详细了解各类 assert 方法,请参阅 Python 的 官方文档。

您也可以借助 Pydoc 模块来学习,类似于 Python 的 help 函数。下面演示了如何使用 Pydoc 查看 assertCountEqual 方法的文档。

!python -m pydoc unittest.TestCase.assertCountEqual

输出:

Help on function assertCountEqual in unittest.TestCase:
unittest.TestCase.assertCountEqual = assertCountEqual(self, first, second, msg=None)
    An unordered sequence comparison asserting that the same elements,
    regardless of order.  If the same element occurs more than once,
    it verifies that the elements occur the same number of times.
        self.assertEqual(Counter(list(first)),
                         Counter(list(second)))
     Example:
        - [0, 1, 1] and [1, 0, 1] compare equal.
        - [0, 0, 1] and [0, 1] compare unequal.

结语

本教程简要介绍了 Python 中的单元测试及其对开发者的重要性。一个不错的练习是:为您以往的任一项目编写一个单元测试模块。这将帮助您亲手实践单元测试的编写。另外,也请尝试探索其余的断言方法。想进一步了解单元测试,请阅读我们的 Pytest 教程。

如果您刚开始学习 Python,想进一步入门,请参加我们的 Python 数据科学入门课程。

Abid Ali Awan

作者

Abid Ali Awan

作为一名持证的数据科学家,我热衷于利用前沿技术打造创新的机器学习应用。凭借在语音识别、数据分析与报告、MLOps、对话式人工智能以及自然语言处理方面的扎实背景,我不断打磨构建智能系统的能力,力求带来切实影响。除技术专长外,我也擅长沟通,能够将复杂概念提炼为清晰、简明的表述。因此,我成为数据科学领域备受关注的博主,与不断壮大的数据专业人士社区分享洞见与实践经验。目前,我专注于内容创作与编辑,借助大语言模型打造有力且吸引人的内容,帮助企业与个人更好地发挥数据价值。

主题