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

作者选择了COVID-19 救济基金来接受捐赠,作为Write for DOnations计划的一部分。

介绍

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

使用该unittest模块编写的测试可以帮助您找到程序中的错误,并防止随着时间的推移更改代码时发生回归。坚持测试驱动开发的团队可能会发现unittest确保所有编写的代码都有一组相应的测试很有用。

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

先决条件

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

定义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.TestCasetest_add_fish_to_aquarium_success上定义了一个命名方法TestAddFishToAquarium使用特定输入test_add_fish_to_aquarium_success调用add_fish_to_aquarium函数并验证实际返回值是否与我们期望返回的值匹配。

现在我们已经定义了一个TestCase带有测试子类,让我们回顾一下如何执行该测试。

执行一个 TestCase

在上一节中,我们创建了一个TestCase名为子类TestAddFishToAquarium从与test_add_fish_to_aquarium.py文件相同的目录中,让我们使用以下命令运行该测试:

  • python -m unittest test_add_fish_to_aquarium.py

我们调用名为Python库模块unittestpython -m unittest然后,我们提供了包含我们的文件的路径TestAddFishToAquarium TestCase作为参数。

运行此命令后,我们会收到如下输出:

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

unittest模块运行了我们的测试并告诉我们我们的测试运行了OK.输出第一行的单曲代表我们通过的测试。

注意: TestCase将测试方法识别为任何以 开头的方法test例如,def test_add_fish_to_aquarium_success(self)被识别为测试并将照此运行。def example_test(self)相反,不会被识别为测试,因为它不以 开头test只有以 开头的方法test才会在您运行时运行并报告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不会"rabbit"在其属于 的鱼列表中返回"tank_a"让我们运行测试。

再次,从test_add_fish_to_aquarium.py我们运行的同一目录

  • python -m unittest test_add_fish_to_aquarium.py

当我们运行此命令时,我们会收到如下输出:

Output
F ====================================================================== 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']}另请注意,.输出的第一行现在有一个,而不是 a 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函数,但它使用包含"shark"重复 25 次的字符串的 25 个元素长列表来调用

test_add_fish_to_aquarium_exception使用由提供with self.assertRaises(...) 上下文管理器TestCase来检查add_fish_to_aquarium拒绝输入的列表太长。的第一个参数self.assertRaises是我们期望引发的 Exception 类——在本例中,ValueError. self.assertRaises上下文管理被绑定到一个指定的变量exception_contextexception属性上exception_context包含底层ValueErroradd_fish_to_aquarium提高。当我们调用str()ValueError来检索它的消息时,它返回我们期望的正确异常消息。

从与 相同的目录中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包含了许多超越其他方法assertEqual,并assertRaises可以使用。断言方法的完整列表可以在文档中找到,但这里包含了一个选择:

方法 断言
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,但可以True通过调用设置为FishTank.fill_with_water()TestCase子类TestFishTank定义了一个名为方法setUp实例化一个新的FishTank实例,并转让该实例self.fish_tank

由于setUp在每个单独的测试方法之前运行,FishTank因此为test_fish_tank_empty_by_default实例化了一个新实例test_fish_tank_can_be_filledtest_fish_tank_empty_by_default验证has_waterFalse. 调用 后test_fish_tank_can_be_filled验证has_water设置为Truefill_with_water()

从与 相同的目录中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支持setUp名为方法的对应物tearDowntearDown例如,如果我们需要清理与数据库的连接,或者在每次测试完成后对文件系统进行修改,这将非常有用。我们将回顾一个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还公开了一个empty_tank删除fish_tank.txt文件方法TestAdvancedFishTank TestCase子类定义两者setUptearDown方法。

setUp方法创建一个AdvancedFishTank实例并将其分配给self.fish_tanktearDown方法调用empty_tank方法 on self.fish_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具有不同断言的类,使用了setUptearDown方法,并从命令行运行了测试。

unittest模块公开了您在本教程中未涵盖的其他类和实用程序。现在,你有一个底线,你可以使用unittest模块的文档,详细了解其他可用的类和公用事业。您可能还对如何将单元测试添加到您的 Django 项目感兴趣

觉得文章有用?

点个广告表达一下你的爱意吧 !😁