如何使用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 项目 感兴趣。