Python 单元测试模块

Beginner

This tutorial is from open-source community. Access the source code

简介

在这个实验中,你将学习如何使用 Python 的内置 unittest 模块。该模块提供了一个用于组织和运行测试的框架,这是软件开发中确保代码正确运行的关键部分。

你还将学习创建和运行基本的测试用例,以及测试预期的错误和异常。unittest 模块简化了创建测试套件、运行测试和验证结果的过程。在这个实验中,将创建 teststock.py 文件。

创建你的第一个单元测试

Python 的 unittest 模块是一个强大的工具,它提供了一种结构化的方式来组织和执行测试。在我们开始编写第一个单元测试之前,让我们先了解一些关键概念。测试固件(Test fixtures)是像 setUptearDown 这样的方法,它们有助于在测试前准备环境,并在测试后清理环境。测试用例(Test cases)是单独的测试单元,测试套件(Test suites)是测试用例的集合,而测试运行器(Test runners)负责执行这些测试并展示结果。

在这第一步中,我们将为 Stock 类创建一个基本的测试文件,该类已经在 stock.py 文件中定义好了。

  1. 首先,让我们打开 stock.py 文件。这将帮助我们了解要测试的 Stock 类。通过查看 stock.py 中的代码,我们可以了解该类的结构、它有哪些属性以及提供了哪些方法。要查看 stock.py 文件的内容,请在终端中运行以下命令:
cat stock.py
  1. 现在,是时候使用你喜欢的文本编辑器创建一个名为 teststock.py 的新文件了。这个文件将包含我们针对 Stock 类的测试用例。以下是你需要在 teststock.py 文件中编写的代码:
## teststock.py

import unittest
import stock

class TestStock(unittest.TestCase):
    def test_create(self):
        s = stock.Stock('GOOG', 100, 490.1)
        self.assertEqual(s.name, 'GOOG')
        self.assertEqual(s.shares, 100)
        self.assertEqual(s.price, 490.1)

if __name__ == '__main__':
    unittest.main()

让我们来分析一下这段代码的关键部分:

  • import unittest:这行代码导入了 unittest 模块,该模块为在 Python 中编写和运行测试提供了必要的工具和类。
  • import stock:这行代码导入了包含我们 Stock 类的模块。如果没有这个导入,我们就无法在测试代码中访问 Stock 类。
  • class TestStock(unittest.TestCase):我们创建了一个名为 TestStock 的新类,它继承自 unittest.TestCase。这使得我们的 TestStock 类成为一个测试用例类,它可以包含多个测试方法。
  • def test_create(self):这是一个测试方法。在 unittest 框架中,所有的测试方法都必须以 test_ 作为前缀。这个方法创建了一个 Stock 类的实例,然后使用 assertEqual 方法来检查 Stock 实例的属性是否与预期值匹配。
  • assertEqual:这是 TestCase 类提供的一个方法。它用于检查两个值是否相等。如果它们不相等,测试将失败。
  • unittest.main():当直接执行这个脚本时,unittest.main() 将运行 TestStock 类中的所有测试方法并显示结果。
  1. teststock.py 文件中编写完代码后,保存它。然后,在终端中运行以下命令来执行测试:
python3 teststock.py

你应该会看到类似于以下的输出:

.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

输出中的单个点 (.) 表示一个测试已成功通过。如果测试失败,你将看到一个 F 而不是点,同时还会有关于测试中出现问题的详细信息。这个输出有助于你快速确定你的代码是否按预期工作,或者是否有需要修复的问题。

扩展你的测试用例

既然你已经创建了一个基本的测试用例,现在是时候扩大你的测试范围了。添加更多的测试将有助于你覆盖 Stock 类的其余功能。这样,你可以确保该类的所有方面都能按预期工作。我们将修改 TestStock 类,以包含对几个方法和属性的测试。

  1. 打开 teststock.py 文件。在 TestStock 类中,我们将添加一些新的测试方法。这些方法将测试 Stock 类的不同部分。以下是你需要添加的代码:
def test_create_keyword_args(self):
    s = stock.Stock(name='GOOG', shares=100, price=490.1)
    self.assertEqual(s.name, 'GOOG')
    self.assertEqual(s.shares, 100)
    self.assertEqual(s.price, 490.1)

def test_cost(self):
    s = stock.Stock('GOOG', 100, 490.1)
    self.assertEqual(s.cost, 49010.0)

def test_sell(self):
    s = stock.Stock('GOOG', 100, 490.1)
    s.sell(20)
    self.assertEqual(s.shares, 80)

def test_from_row(self):
    row = ['GOOG', '100', '490.1']
    s = stock.Stock.from_row(row)
    self.assertEqual(s.name, 'GOOG')
    self.assertEqual(s.shares, 100)
    self.assertEqual(s.price, 490.1)

def test_repr(self):
    s = stock.Stock('GOOG', 100, 490.1)
    self.assertEqual(repr(s), "Stock('GOOG', 100, 490.1)")

def test_eq(self):
    s1 = stock.Stock('GOOG', 100, 490.1)
    s2 = stock.Stock('GOOG', 100, 490.1)
    self.assertEqual(s1, s2)

让我们仔细看看这些测试各自的作用:

  • test_create_keyword_args:此测试检查你是否可以使用关键字参数创建一个 Stock 对象。它验证对象的属性是否设置正确。
  • test_cost:此测试检查 Stock 对象的 cost 属性是否返回正确的值,该值是通过股数乘以价格计算得出的。
  • test_sell:此测试检查 Stock 对象的 sell() 方法在卖出部分股票后是否正确更新股数。
  • test_from_row:此测试检查 from_row() 类方法是否可以从数据行创建一个新的 Stock 实例。
  • test_repr:此测试检查 Stock 对象的 __repr__() 方法是否返回预期的字符串表示形式。
  • test_eq:此测试检查 __eq__() 方法是否能正确比较两个 Stock 对象是否相等。
  1. 添加这些测试方法后,保存 teststock.py 文件。然后,在终端中使用以下命令再次运行测试:
python3 teststock.py

如果所有测试都通过,你应该会看到如下输出:

......
----------------------------------------------------------------------
Ran 7 tests in 0.001s

OK

输出中的七个点代表每个测试。每个点表示一个测试已成功通过。所以,如果你看到七个点,就意味着所有七个测试都通过了。

异常测试

测试是软件开发中至关重要的一部分,其中一个重要方面是确保你的代码能够正确处理错误情况。在 Python 中,unittest 模块提供了一种便捷的方式来测试特定异常是否按预期抛出。

  1. 打开 teststock.py 文件。我们将添加一些用于检查异常的测试方法。这些测试将帮助我们确保代码在遇到无效输入时能正常运行。
def test_shares_type(self):
    s = stock.Stock('GOOG', 100, 490.1)
    with self.assertRaises(TypeError):
        s.shares = '50'

def test_shares_value(self):
    s = stock.Stock('GOOG', 100, 490.1)
    with self.assertRaises(ValueError):
        s.shares = -50

def test_price_type(self):
    s = stock.Stock('GOOG', 100, 490.1)
    with self.assertRaises(TypeError):
        s.price = '490.1'

def test_price_value(self):
    s = stock.Stock('GOOG', 100, 490.1)
    with self.assertRaises(ValueError):
        s.price = -490.1

def test_attribute_error(self):
    s = stock.Stock('GOOG', 100, 490.1)
    with self.assertRaises(AttributeError):
        s.share = 100  ## 'share' is incorrect, should be 'shares'

现在,让我们来了解这些异常测试是如何工作的。

  • with self.assertRaises(ExceptionType): 语句创建了一个上下文管理器。这个上下文管理器会检查 with 块内的代码是否抛出了指定的异常。
  • 如果在 with 块内抛出了预期的异常,测试就会通过。这意味着我们的代码能够正确检测到无效输入并抛出相应的错误。
  • 如果没有抛出异常或者抛出了不同的异常,测试就会失败。这表明我们的代码可能没有按预期处理无效输入。

这些测试旨在验证以下场景:

  • shares 属性设置为字符串应该抛出 TypeError,因为 shares 应该是一个数字。
  • shares 属性设置为负数应该抛出 ValueError,因为股数不能为负数。
  • price 属性设置为字符串应该抛出 TypeError,因为 price 应该是一个数字。
  • price 属性设置为负数应该抛出 ValueError,因为价格不能为负数。
  • 尝试设置一个不存在的属性 share(注意缺少 's')应该抛出 AttributeError,因为正确的属性名是 shares
  1. 添加这些测试方法后,保存 teststock.py 文件。然后,在终端中使用以下命令运行所有测试:
python3 teststock.py

如果一切正常,你应该会看到输出表明所有 12 个测试都已通过。输出将如下所示:

............
----------------------------------------------------------------------
Ran 12 tests in 0.002s

OK

这十二个点代表你到目前为止编写的所有测试。上一步有 7 个测试,我们刚刚又添加了 5 个。这个输出表明你的代码正在按预期处理异常,这是一个经过充分测试的程序的良好标志。

运行选定的测试并使用测试发现功能

Python 中的 unittest 模块是一个强大的工具,可让你有效地测试代码。它提供了多种方式来运行特定的测试,或者自动发现并运行项目中的所有测试。这非常有用,因为它能帮助你在测试时专注于代码的特定部分,或者快速检查整个项目的测试套件。

运行特定的测试

有时,你可能只想运行特定的测试方法或测试类,而不是整个测试套件。你可以通过在 unittest 模块中使用模式选项来实现这一点。这能让你更好地控制执行哪些测试,在调试代码的特定部分时非常方便。

  1. 若要仅运行与创建 Stock 对象相关的测试:
python3 -m unittest teststock.TestStock.test_create

在这个命令中,python3 -m unittest 告诉 Python 运行 unittest 模块。teststock 是测试文件的名称,TestStock 是测试类的名称,test_create 是我们想要运行的特定测试方法。通过运行这个命令,你可以快速检查与创建 Stock 对象相关的代码是否按预期工作。

  1. 若要运行 TestStock 类中的所有测试:
python3 -m unittest teststock.TestStock

在这里,我们省略了具体的测试方法名称。因此,这个命令将执行 teststock 文件中 TestStock 类内的所有测试方法。当你想检查 Stock 对象测试用例的整体功能时,这很有用。

使用测试发现功能

unittest 模块可以自动发现并运行项目中的所有测试文件。这省去了你手动指定每个要运行的测试文件的麻烦,特别是在有许多测试文件的大型项目中。

  1. 重命名当前文件以遵循测试发现的命名模式:
mv teststock.py test_stock.py

unittest 中的测试发现机制会查找遵循 test_*.py 命名模式的文件。通过将文件重命名为 test_stock.py,我们让 unittest 模块更容易找到并运行该文件中的测试。

  1. 运行测试发现:
python3 -m unittest discover

这个命令告诉 unittest 模块自动发现并运行当前目录中所有符合 test_*.py 模式的测试文件。它会遍历目录并执行在匹配文件中找到的所有测试用例。

  1. 你还可以指定一个目录来搜索测试:
python3 -m unittest discover -s . -p "test_*.py"

其中:

  • -s . 指定开始发现测试的目录(在这种情况下是当前目录)。点号 (.) 表示当前目录。如果你想在其他位置搜索测试,可以将其更改为其他目录路径。
  • -p "test_*.py" 是匹配测试文件的模式。这确保只有以 test_ 开头且扩展名为 .py 的文件才会被视为测试文件。

你应该会看到和之前一样,所有 12 个测试都运行并通过了。

  1. 为了与实验保持一致,将文件重命名回原来的名称:
mv test_stock.py teststock.py

运行测试发现后,我们将文件重命名回原来的名称,以保持实验环境的一致性。

通过使用测试发现功能,你可以轻松运行项目中的所有测试,而无需单独指定每个测试文件。这使测试过程更高效,也更不容易出错。

总结

在本次实验中,你学习了如何使用 Python 的 unittest 模块来创建和运行自动化测试。你通过继承 unittest.TestCase 类创建了一个基本的测试用例,编写了测试来验证类的方法和属性的正常功能,还创建了测试来检查在错误情况下是否抛出了适当的异常。你还学习了如何运行特定的测试以及如何使用测试发现功能。

单元测试是软件开发中的一项基本技能,它能确保代码的可靠性和正确性。编写全面的测试有助于尽早发现 bug,并让你对代码的行为更有信心。在开发 Python 应用程序时,你可以考虑采用测试驱动开发(TDD)的方法,即在实现功能之前先编写测试,这样可以让代码更健壮、更易于维护。