如何使用 unittest 为 Python 函数编写测试用例

作者选择了 COVID-19 救援基金作为 Write for Donations计划的一部分接受捐款。

介绍

Python 标准库包括unittest模块,以帮助您为 Python 代码编写和运行测试。

使用unittest模块编写的测试可以帮助您在程序中找到错误,并防止随着时间的推移而发生回归。

在本教程中,您将使用Python的unittest模块来写一个函数的测试。

前提条件

为了充分利用本教程,您将需要:

您可以查看 如何在Python中定义函数 3]教程,该教程是 如何在Python中编码 3系列的一部分。

定义一个测试案例子类

unittest模块提供的最重要的类别之一被称为TestCaseTestCase提供了测试我们的功能的通用支架。

 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' 以外的一些其他方法。

MethodAssertion
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_defaulttest_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_tanktearDown方法在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类以不同的声明,使用了SetUptearDown方法,并从命令行运行您的测试。

unittest模块揭示了您在本教程中未涵盖的其他类别和实用工具. 现在您有一个基线,您可以使用unittest模块的文档(https://docs.python.org/3/library/unittest.html)来了解更多有关其他可用的类别和实用工具。

Published At
Categories with 技术
Tagged with
comments powered by Disqus