作者选择了 COVID-19 救援基金作为 Write for Donations计划的一部分接受捐款。
介绍
Python 标准库包括unittest
模块,以帮助您为 Python 代码编写和运行测试。
使用unittest
模块编写的测试可以帮助您在程序中找到错误,并防止随着时间的推移而发生回归。
在本教程中,您将使用Python的unittest
模块来写一个函数的测试。
前提条件
为了充分利用本教程,您将需要:
您可以查看 如何在Python中定义函数 3]教程,该教程是 如何在Python中编码 3系列的一部分。
定义一个测试案例
子类
由unittest
模块提供的最重要的类别之一被称为TestCase
。TestCase
提供了测试我们的功能的通用支架。
1[label test_add_fish_to_aquarium.py]
2import unittest
3
4def add_fish_to_aquarium(fish_list):
5 if len(fish_list) > 10:
6 raise ValueError("A maximum of 10 fish can be added to the aquarium")
7 return {"tank_a": fish_list}
8
9class TestAddFishToAquarium(unittest.TestCase):
10 def test_add_fish_to_aquarium_success(self):
11 actual = add_fish_to_aquarium(fish_list=["shark", "tuna"])
12 expected = {"tank_a": ["shark", "tuna"]}
13 self.assertEqual(actual, expected)
首先我们导入unittest
,使模块可用于我们的代码,然后我们定义了我们想要测试的函数,这里是add_fish_to_aquarium
。
在这种情况下,我们的add_fish_to_aquarium
函数接受名为fish_list
的鱼类列表,并且如果fish_list
包含超过10个元素,会引出错误。
一个名为TestAddFishToAquarium
的类被定义为unittest.TestCase
的子类.一个名为test_add_fish_to_aquarium_success
的方法被定义为TestAddFishToAquarium
。
现在我们已经定义了与测试的TestCase
子类,让我们看看我们如何执行该测试。
执行测试
在上一节中,我们创建了一个名为TestCase
的子类,名为TestAddFishToAquarium
。从同一个目录中作为test_add_fish_to_aquarium.py
文件,让我们用以下命令运行这个测试:
1python -m unittest test_add_fish_to_aquarium.py
然后,我们提供了包含我们的TestAddFishToAquarium``TestCase
作为论点的文件路径。
运行此命令后,我们会收到如下的输出:
1[secondary_label Output]
2.
3----------------------------------------------------------------------
4Ran 1 test in 0.000s
5
6OK
unittest
模块运行了我们的测试,并告诉我们我们的测试运行了OK
。输出第一行上的单词代表了我们通过的测试。
例如,def test_add_fish_to_aquarium_success(自我)将被识别为测试,并将作为测试运行。‘def example_test(自我)’相反,不会被识别为测试,因为它不会从‘test’开始。
现在让我们尝试一个失败的测试。
我们在我们的测试方法中修改了以下突出的行,以引入失败:
1[label test_add_fish_to_aquarium.py]
2import unittest
3
4def add_fish_to_aquarium(fish_list):
5 if len(fish_list) > 10:
6 raise ValueError("A maximum of 10 fish can be added to the aquarium")
7 return {"tank_a": fish_list}
8
9class TestAddFishToAquarium(unittest.TestCase):
10 def test_add_fish_to_aquarium_success(self):
11 actual = add_fish_to_aquarium(fish_list=["shark", "tuna"])
12 expected = {"tank_a": ["rabbit"]}
13 self.assertEqual(actual, expected)
修改后的测试将失败,因为add_fish_to_aquarium
不会在其tank_a
鱼类列表中返回兔子
。
再一次,从同一个目录为 test_add_fish_to_aquarium.py
我们运行:
1python -m unittest test_add_fish_to_aquarium.py
当我们运行这个命令时,我们会收到如下的输出:
1[secondary_label Output]
2F
3======================================================================
4FAIL: test_add_fish_to_aquarium_success (test_add_fish_to_aquarium.TestAddFishToAquarium)
5----------------------------------------------------------------------
6Traceback (most recent call last):
7 File "test_add_fish_to_aquarium.py", line 13, in test_add_fish_to_aquarium_success
8 self.assertEqual(actual, expected)
9AssertionError: {'tank_a': ['shark', 'tuna']} != {'tank_a': ['rabbit']}
10- {'tank_a': ['shark', 'tuna']}
11+ {'tank_a': ['rabbit']}
12
13----------------------------------------------------------------------
14Ran 1 test in 0.001s
15
16FAILED (failures=1)
错误的输出表明,我们的测试失败了。 实际的输出 {'tank_a': ['shark', 'tuna']}
没有匹配我们在 test_add_fish_to_aquarium.py
中添加的(错误的)预期: {'tank_a': ['rabbit']
。 另外,请注意,而不是 .
,输出的第一行现在有 F
. 虽然 .
字符在通过测试时输出,而 unittest 运行一个失败的测试时的输出是
F`。
现在我们已经编写并运行了一个测试,让我们尝试为add_fish_to_aquarium
函数的不同行为编写另一个测试。
测试引发例外的函数
unittest
还可以帮助我们验证add_fish_to_aquarium
函数会引发ValueError
例外,如果输入过多的鱼。
1[label test_add_fish_to_aquarium.py]
2import unittest
3
4def add_fish_to_aquarium(fish_list):
5 if len(fish_list) > 10:
6 raise ValueError("A maximum of 10 fish can be added to the aquarium")
7 return {"tank_a": fish_list}
8
9class TestAddFishToAquarium(unittest.TestCase):
10 def test_add_fish_to_aquarium_success(self):
11 actual = add_fish_to_aquarium(fish_list=["shark", "tuna"])
12 expected = {"tank_a": ["shark", "tuna"]}
13 self.assertEqual(actual, expected)
14
15 def test_add_fish_to_aquarium_exception(self):
16 too_many_fish = ["shark"] * 25
17 with self.assertRaises(ValueError) as exception_context:
18 add_fish_to_aquarium(fish_list=too_many_fish)
19 self.assertEqual(
20 str(exception_context.exception),
21 "A maximum of 10 fish can be added to the aquarium"
22 )
新的测试方法 test_add_fish_to_aquarium_exception
也召唤了 add_fish_to_aquarium
函数,但它这样做的是包含字符串 `"鲨鱼" 的 25 个元素长列表重复了 25 次。
「test_add_fish_to_aquarium_exception」使用「with self.assertRaises(...)」 context manager」來檢查「add_fish_to_aquarium」是否會拒絕輸入列表的時間太長。「self.assertRaises」的第一個論點是我們預期會提出的例外類別──在這種情況下是「ValueError」。「self.assertRaises」的背景管理器與名為「exception_context」的變量有關。在「exception_context」上的「例外」屬性包含了「ValueError」的底部,而「addish_fish_to_aquarium」是提出的。當我們在該「ValueError」上呼叫「str()」
从同一个目录为 test_add_fish_to_aquarium.py
,让我们运行我们的测试:
1python -m unittest test_add_fish_to_aquarium.py
当我们运行这个命令时,我们会收到如下的输出:
1[secondary_label Output]
2..
3----------------------------------------------------------------------
4Ran 2 tests in 0.000s
5
6OK
值得注意的是,如果add_fish_to_aquarium
没有提到例外,或者提到不同的例外,我们的测试将失败(例如TypeError
而不是ValueError
)。
<$>[注]
注: unittest.TestCase
揭示了您可以使用的 assertEqual
和 `assertRaises' 以外的一些其他方法。
Method | Assertion |
---|---|
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
提供的其他工具来利用我们正在测试的任何代码。
使用设置
方法创建资源
TestCase
还支持一个setUp
方法,以帮助您在每个测试的基础上创建资源。setUp
方法有助于当您有一个共同的准备代码,您想在每个测试之前运行时。
让我们来看看一个例子:
1[label test_fish_tank.py]
2import unittest
3
4class FishTank:
5 def __init__(self):
6 self.has_water = False
7
8 def fill_with_water(self):
9 self.has_water = True
10
11class TestFishTank(unittest.TestCase):
12 def setUp(self):
13 self.fish_tank = FishTank()
14
15 def test_fish_tank_empty_by_default(self):
16 self.assertFalse(self.fish_tank.has_water)
17
18 def test_fish_tank_can_be_filled(self):
19 self.fish_tank.fill_with_water()
20 self.assertTrue(self.fish_tank.has_water)
test_fish_tank.py
定义了一个名为 FishTank
的类别。 FishTank.has_water
最初设置为 False,但可以通过调用
FishTank.fill_with_water()设置为
True。
由于SetUp
在每个单独的测试方法之前运行,因此对test_fish_tank_empty_by_default
和test_fish_tank_can_be_filled
进行实例化新的FishTank
实例。
从同一个目录为 test_fish_tank.py
,我们可以运行:
1python -m unittest test_fish_tank.py
如果我们运行上一个命令,我们将收到以下输出:
1[secondary_label Output]
2..
3----------------------------------------------------------------------
4Ran 2 tests in 0.000s
5
6OK
最终的结果表明,这两个测试都通过了。
SetUp
允许我们编写为我们所有测试运行的准备代码在一个TestCase
子类。
<$>[注]
注: 如果您有多个测试文件具有TestCase
子类,您想运行,请考虑使用python -m unittest discover
运行多个测试文件。
使用tearDown
方法清理资源
「TestCase」支持名为「tearDown」的「setUp」方法的对称。「tearDown」是有用的,例如,如果我们需要清理到数据库的连接,或者在每个测试完成后对文件系统进行更改。
1[label test_advanced_fish_tank.py]
2import os
3import unittest
4
5class AdvancedFishTank:
6 def __init__(self):
7 self.fish_tank_file_name = "fish_tank.txt"
8 default_contents = "shark, tuna"
9 with open(self.fish_tank_file_name, "w") as f:
10 f.write(default_contents)
11
12 def empty_tank(self):
13 os.remove(self.fish_tank_file_name)
14
15class TestAdvancedFishTank(unittest.TestCase):
16 def setUp(self):
17 self.fish_tank = AdvancedFishTank()
18
19 def tearDown(self):
20 self.fish_tank.empty_tank()
21
22 def test_fish_tank_writes_file(self):
23 with open(self.fish_tank.fish_tank_file_name) as f:
24 contents = f.read()
25 self.assertEqual(contents, "shark, tuna")
「test_advanced_fish_tank.py」定义了一个名为「AdvancedFishTank」的类。「AdvancedFishTank」创建了一个名为「fish_tank.txt」的文件,并写入字符串「shark, tuna」。
setUp
方法创建了一个AdvancedFishTank
实例,并将其分配给self.fish_tank
。tearDown
方法在self.fish_tank
上调用了empty_tank
方法:这确保在每个测试方法运行后删除fish_tank.txt
文件。
从同一个目录中作为 test_advanced_fish_tank.py
让我们运行:
1python -m unittest test_advanced_fish_tank.py
我们将获得以下产出:
1[secondary_label Output]
2.
3----------------------------------------------------------------------
4Ran 1 test in 0.000s
5
6OK
tearDown
允许您写清理代码,在一个TestCase
子类中运行所有测试。
结论
在本教程中,您已经编写了TestCase
类以不同的声明,使用了SetUp
和tearDown
方法,并从命令行运行您的测试。
该unittest
模块揭示了您在本教程中未涵盖的其他类别和实用工具. 现在您有一个基线,您可以使用unittest
模块的文档(https://docs.python.org/3/library/unittest.html)来了解更多有关其他可用的类别和实用工具。