如何使用unittest为Python中的函数编写测试用例
作者选择了 COVID-19 Relief Fund 作为 Write for DOnations 计划的一部分来接受捐赠。
介绍
Python 标准库包括 unittest 模块,可帮助您为 Python 代码编写和运行测试。
使用 unittest 模块编写的测试可以帮助您发现程序中的错误,并防止随着时间的推移更改代码而发生回归。 坚持 测试驱动开发 的团队可能会发现 unittest 有助于确保所有编写的代码都有相应的测试集。
在本教程中,您将使用 Python 的 unittest 模块来编写函数测试。
先决条件
要充分利用本教程,您需要:
- 了解 Python 中的函数。 您可以查看 如何在 Python 3 中定义函数教程,它是 如何在 Python 3 中编码系列的一部分。
定义 TestCase 子类
unittest 模块提供的最重要的类之一被命名为 TestCase。 TestCase 为测试我们的功能提供了通用的脚手架。 让我们考虑一个例子:
test_add_fish_to_aquarium.py
import unittest
def add_fish_to_aquarium(fish_list):
if len(fish_list) > 10:
raise ValueError("A maximum of 10 fish can be added to the aquarium")
return {"tank_a": fish_list}
class TestAddFishToAquarium(unittest.TestCase):
def test_add_fish_to_aquarium_success(self):
actual = add_fish_to_aquarium(fish_list=["shark", "tuna"])
expected = {"tank_a": ["shark", "tuna"]}
self.assertEqual(actual, expected)
首先我们导入 unittest 以使模块可用于我们的代码。 然后我们定义我们想要测试的函数——这里是 add_fish_to_aquarium。
在这种情况下,我们的 add_fish_to_aquarium 函数接受名为 fish_list 的鱼列表,如果 fish_list 有超过 10 个元素,则会引发错误。 然后该函数返回一个字典,将鱼缸的名称 "tank_a" 映射到给定的 fish_list。
一个名为 TestAddFishToAquarium 的类被定义为 unittest.TestCase 的子类。 在 TestAddFishToAquarium 上定义了一个名为 test_add_fish_to_aquarium_success 的方法。 test_add_fish_to_aquarium_success 使用特定输入调用 add_fish_to_aquarium 函数,并验证实际返回的值是否与我们期望返回的值匹配。
现在我们已经定义了一个带有测试的 TestCase 子类,让我们回顾一下如何执行该测试。
执行 TestCase
在上一节中,我们创建了一个名为 TestAddFishToAquarium 的 TestCase 子类。 从与 test_add_fish_to_aquarium.py 文件相同的目录中,让我们使用以下命令运行该测试:
python -m unittest test_add_fish_to_aquarium.py
我们使用 python -m unittest 调用了名为 unittest 的 Python 库模块。 然后,我们提供了包含我们的 TestAddFishToAquarium TestCase 作为参数的文件的路径。
运行此命令后,我们会收到如下输出:
Output. ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
unittest 模块运行了我们的测试并告诉我们我们的测试运行了 OK。 输出第一行的单个 . 代表我们通过的测试。
笔记: TestCase recognizes test methods as any method that begins with test. For example, def test_add_fish_to_aquarium_success(self) is recognized as a test and will be run as such. def example_test(self), conversely, would not be recognized as a test because it does not begin with test. Only methods beginning with test will be run and reported when you run python -m unittest ....
现在让我们尝试一个失败的测试。
我们在测试方法中修改以下突出显示的行以引入失败:
test_add_fish_to_aquarium.py
import unittest
def add_fish_to_aquarium(fish_list):
if len(fish_list) > 10:
raise ValueError("A maximum of 10 fish can be added to the aquarium")
return {"tank_a": fish_list}
class TestAddFishToAquarium(unittest.TestCase):
def test_add_fish_to_aquarium_success(self):
actual = add_fish_to_aquarium(fish_list=["shark", "tuna"])
expected = {"tank_a": ["rabbit"]}
self.assertEqual(actual, expected)
修改后的测试将失败,因为 add_fish_to_aquarium 不会在其属于 "tank_a" 的鱼列表中返回 "rabbit"。 让我们运行测试。
同样,从与 test_add_fish_to_aquarium.py 相同的目录中,我们运行:
python -m unittest test_add_fish_to_aquarium.py
当我们运行这个命令时,我们会收到如下输出:
OutputF
======================================================================
FAIL: test_add_fish_to_aquarium_success (test_add_fish_to_aquarium.TestAddFishToAquarium)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_add_fish_to_aquarium.py", line 13, in test_add_fish_to_aquarium_success
self.assertEqual(actual, expected)
AssertionError: {'tank_a': ['shark', 'tuna']} != {'tank_a': ['rabbit']}
- {'tank_a': ['shark', 'tuna']}
+ {'tank_a': ['rabbit']}
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
失败输出表明我们的测试失败了。 {'tank_a': ['shark', 'tuna']} 的实际输出与我们添加到 test_add_fish_to_aquarium.py 的(不正确的)期望不匹配:{'tank_a': ['rabbit']}。 另请注意,输出的第一行现在有一个 F,而不是 .。 测试通过时输出 . 字符,而 F 是 unittest 运行测试失败时的输出。
现在我们已经编写并运行了一个测试,让我们尝试为 add_fish_to_aquarium 函数的不同行为编写另一个测试。
测试引发异常的函数
unittest 还可以帮助我们验证 add_fish_to_aquarium 函数在输入太多鱼时是否会引发 ValueError 异常。 让我们扩展之前的示例,并添加一个名为 test_add_fish_to_aquarium_exception 的新测试方法:
test_add_fish_to_aquarium.py
import unittest
def add_fish_to_aquarium(fish_list):
if len(fish_list) > 10:
raise ValueError("A maximum of 10 fish can be added to the aquarium")
return {"tank_a": fish_list}
class TestAddFishToAquarium(unittest.TestCase):
def test_add_fish_to_aquarium_success(self):
actual = add_fish_to_aquarium(fish_list=["shark", "tuna"])
expected = {"tank_a": ["shark", "tuna"]}
self.assertEqual(actual, expected)
def test_add_fish_to_aquarium_exception(self):
too_many_fish = ["shark"] * 25
with self.assertRaises(ValueError) as exception_context:
add_fish_to_aquarium(fish_list=too_many_fish)
self.assertEqual(
str(exception_context.exception),
"A maximum of 10 fish can be added to the aquarium"
)
新的测试方法 test_add_fish_to_aquarium_exception 也调用了 add_fish_to_aquarium 函数,但它使用包含重复 25 次的字符串 "shark" 的 25 个元素的长列表来执行此操作。
test_add_fish_to_aquarium_exception 使用 TestCase 提供的 with self.assertRaises(...) 上下文管理器 来检查 add_fish_to_aquarium 是否拒绝输入的列表太长。 self.assertRaises 的第一个参数是我们期望引发的异常类——在本例中为 ValueError。 self.assertRaises 上下文管理器绑定到名为 exception_context 的变量。 exception_context 上的 exception 属性包含 add_fish_to_aquarium 引发的基础 ValueError。 当我们在 ValueError 上调用 str() 以检索其消息时,它会返回我们预期的正确异常消息。
在与 test_add_fish_to_aquarium.py 相同的目录中,让我们运行我们的测试:
python -m unittest test_add_fish_to_aquarium.py
当我们运行这个命令时,我们会收到如下输出:
Output.. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK
值得注意的是,如果 add_fish_to_aquarium 没有引发异常,或者引发了不同的异常(例如 TypeError 而不是 ValueError),我们的测试就会失败。
笔记: unittest.TestCase exposes a number of other methods beyond assertEqual and assertRaises that you can use. The full list of assertion methods can be found 在文档中, but a selection are included here:
| 方法 | 断言 |
|---|---|
assertEqual(a, b)
|
a == b
|
assertNotEqual(a, b)
|
a != b
|
assertTrue(a)
|
bool(a) is True
|
assertFalse(a)
|
bool(a) is False
|
assertIsNone(a)
|
a is None
|
assertIsNotNone(a)
|
a is not None
|
assertIn(a, b)
|
a in b
|
assertNotIn(a, b)
|
a not in b
|
现在我们已经编写了一些基本测试,让我们看看如何使用 TestCase 提供的其他工具来利用我们正在测试的任何代码。
使用setUp方法创建资源
TestCase 还支持 setUp 方法来帮助您基于每个测试创建资源。 setUp 方法在您有一组通用的准备代码并希望在每次测试之前运行时会很有帮助。 setUp 让您可以将所有这些准备代码放在一个地方,而不是为每个单独的测试一遍又一遍地重复它。
我们来看一个例子:
test_fish_tank.py
import unittest
class FishTank:
def __init__(self):
self.has_water = False
def fill_with_water(self):
self.has_water = True
class TestFishTank(unittest.TestCase):
def setUp(self):
self.fish_tank = FishTank()
def test_fish_tank_empty_by_default(self):
self.assertFalse(self.fish_tank.has_water)
def test_fish_tank_can_be_filled(self):
self.fish_tank.fill_with_water()
self.assertTrue(self.fish_tank.has_water)
test_fish_tank.py 定义了一个名为 FishTank 的类。 FishTank.has_water 最初设置为 False,但可以通过调用 FishTank.fill_with_water() 设置为 True。 TestCase 子类 TestFishTank 定义了一个名为 setUp 的方法,它实例化一个新的 FishTank 实例并将该实例分配给 self.fish_tank。
由于 setUp 在每个单独的测试方法之前运行,因此为 test_fish_tank_empty_by_default 和 test_fish_tank_can_be_filled 实例化一个新的 FishTank 实例。 test_fish_tank_empty_by_default 验证 has_water 以 False 开始。 test_fish_tank_can_be_filled 在调用 fill_with_water() 后验证 has_water 是否设置为 True。
从与 test_fish_tank.py 相同的目录中,我们可以运行:
python -m unittest test_fish_tank.py
如果我们运行前面的命令,我们将收到以下输出:
Output.. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK
最终输出显示两个测试都通过了。
setUp 允许我们编写为 TestCase 子类中的所有测试运行的准备代码。
注意: 如果您有多个带有 TestCase 子类的测试文件要运行,请考虑使用 python -m unittest discover 运行多个测试文件。 运行 python -m unittest discover --help 以获取更多信息。
使用tearDown方法清理资源
TestCase 支持名为 tearDown 的 setUp 方法的对应项。 tearDown 很有用,例如,我们需要清理与数据库的连接,或在每次测试完成后对文件系统进行修改。 我们将回顾一个将 tearDown 与文件系统一起使用的示例:
test_advanced_fish_tank.py
import os
import unittest
class AdvancedFishTank:
def __init__(self):
self.fish_tank_file_name = "fish_tank.txt"
default_contents = "shark, tuna"
with open(self.fish_tank_file_name, "w") as f:
f.write(default_contents)
def empty_tank(self):
os.remove(self.fish_tank_file_name)
class TestAdvancedFishTank(unittest.TestCase):
def setUp(self):
self.fish_tank = AdvancedFishTank()
def tearDown(self):
self.fish_tank.empty_tank()
def test_fish_tank_writes_file(self):
with open(self.fish_tank.fish_tank_file_name) as f:
contents = f.read()
self.assertEqual(contents, "shark, tuna")
test_advanced_fish_tank.py 定义了一个名为 AdvancedFishTank 的类。 AdvancedFishTank 创建一个名为 fish_tank.txt 的文件并将字符串 "shark, tuna" 写入其中。 AdvancedFishTank 还公开了删除 fish_tank.txt 文件的 empty_tank 方法。 TestAdvancedFishTank TestCase 子类定义了 setUp 和 tearDown 方法。
setUp 方法创建一个 AdvancedFishTank 实例并将其分配给 self.fish_tank。 tearDown 方法在 self.fish_tank 上调用 empty_tank 方法:这确保在每个测试方法运行后删除 fish_tank.txt 文件。 这样,每个测试都从一张白纸开始。 test_fish_tank_writes_file 方法验证 "shark, tuna" 的默认内容是否写入 fish_tank.txt 文件。
从与 test_advanced_fish_tank.py 相同的目录中运行:
python -m unittest test_advanced_fish_tank.py
我们将收到以下输出:
Output. ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
tearDown 允许您编写为 TestCase 子类中的所有测试运行的清理代码。
结论
在本教程中,您编写了具有不同断言的 TestCase 类,使用了 setUp 和 tearDown 方法,并从命令行运行测试。
unittest 模块公开了本教程中未涉及的其他类和实用程序。 现在您有了基线,您可以使用 单元测试模块的文档 来了解有关其他可用类和实用程序的更多信息。 您可能还对 如何将单元测试添加到您的 Django 项目 感兴趣。