用unittest和pytest进行Python单元测试的介绍
在这篇文章中,我们将看看什么是软件测试,以及为什么你应该关心它。我们将学习如何设计单元测试以及如何编写Python单元测试。特别是,我们将看看Python中两个最常用的单元测试框架,unittest 和pytest 。
软件测试简介
软件测试是检查软件产品行为的过程,以评估和验证它是否与规范相一致。软件产品可能有数千行代码,以及数百个一起工作的组件。如果有一行不能正常工作,这个错误就会传播并导致其他错误。因此,为了确保一个程序按照它应该有的方式运行,它必须被测试。
由于现代软件可能相当复杂,所以有多个层次的测试来评估正确性的不同方面。正如ISTQB认证测试基础水平大纲所指出的,软件测试有四个层次:
- 单元测试,测试特定的代码行
- 集成测试,测试许多单元之间的集成。
- 系统测试,测试整个系统
- 验收测试,检查是否符合商业目标。
在这篇文章中,我们将讨论单元测试,但在我们深入探讨之前,我想介绍一下软件测试的一个重要原则。
测试显示的是缺陷的存在,而不是缺陷的缺失。
换句话说,即使你运行的所有测试都没有显示出任何故障,这也不能证明你的软件系统是没有缺陷的,也不能证明另一个测试用例不会在你的软件的行为中发现缺陷。
什么是单元测试?
这是测试的第一个层次,也叫组件测试。在这一部分,对单一的软件组件进行测试。根据编程语言的不同,软件单元可能是一个类,一个函数,或一个方法。例如,如果你有一个叫ArithmeticOperations 的Java类,它有multiply 和divide 方法,那么ArithmeticOperations 类的单元测试将需要同时测试multiply 和divide 方法的正确行为。
单元测试通常由软件测试人员执行。为了运行单元测试,软件测试人员(或开发人员)需要访问源代码,因为源代码本身就是被测对象。由于这个原因,这种直接测试源代码的软件测试方法被称为白盒测试。
你可能想知道为什么你要担心软件测试,以及它是否值得。在下一节,我们将分析测试你的软件系统背后的动机。
为什么你应该做单元测试
软件测试的主要优点是它能提高软件质量。软件质量是至关重要的,特别是在一个软件处理我们各种各样的日常活动的世界中。提高软件的质量仍然是一个太模糊的目标。让我们试着更好地说明我们所说的软件质量是什么意思。根据ISO/IEC标准9126-1ISO 9126,软件质量包括这些因素。
- 可靠性
- 功能
- 效率
- 可用性
- 可维护性
- 可移植性
如果你拥有一家公司,软件测试是你应该仔细考虑的一项活动,因为它可能会对你的业务产生影响。例如,在2022年5月,由于车辆的信息娱乐系统出现问题,特斯拉召回了13万辆汽车。这个问题随后通过 "空中 "分发的软件更新得到修复。这些故障花费了公司的时间和金钱,也给客户带来了问题,因为他们有一段时间无法使用他们的汽车。测试软件确实要花钱,但公司也确实可以在技术支持方面节省数百万美元。
单元测试的重点是检查软件的行为是否正确,这意味着检查输入和输出之间的映射是否都正确。作为一个低级别的测试活动,单元测试有助于早期识别错误,以便它们不会被传播到软件系统的更高层次。
单元测试的其他优点包括。
- 简化集成:通过确保所有组件单独工作良好,更容易解决集成问题。
- 尽量减少代码回归:有了大量的测试用例,如果将来对源代码的一些修改会导致问题,那么就更容易定位问题了。
- 提供文档:通过测试输入和输出之间的正确映射,单元测试提供了关于被测方法或类如何工作的文档。
设计一个测试策略
现在让我们看看如何设计一个测试策略。
测试范围的定义
在开始计划一个测试策略之前,有一个重要的问题需要回答。你想测试你的软件系统的哪些部分?
这是一个关键问题,因为详尽的测试是不可能的。出于这个原因,你不可能测试每一个可能的输入和输出,但你应该根据所涉及的风险来确定测试的优先次序。
在定义你的测试范围时,需要考虑到许多因素。
- 风险:如果一个错误影响到这个组件,会有什么商业后果?
- 时间:你希望你的软件产品多长时间能准备好?你有一个最后期限吗?
- 预算:你愿意在测试活动中投入多少钱?
一旦你定义了测试范围,即规定了你应该测试什么,不应该测试什么,你就可以谈一谈一个好的单元测试应该具备的品质。
单元测试的质量
- 快速:单元测试大多是自动执行的,这意味着它们必须是快速的。缓慢的单元测试更有可能被开发人员跳过,因为它们不能提供即时反馈。
- 孤立的:根据定义,单元测试是独立的。他们测试单独的代码单元,他们不依赖于任何外部事物(如文件或网络资源)。
- 可重复的:单元测试是重复执行的,其结果必须在一段时间内保持一致。
- 可靠的:单元测试只有在被测系统中存在错误时才会失败。环境或测试的执行顺序不应该有问题。
- 命名正确:测试的名称应该提供关于测试本身的相关信息。
在深入研究Python的单元测试之前,还缺少最后一步。我们如何组织我们的测试,使其整洁并易于阅读?我们使用一种叫做Arrange, Act and Assert(AAA)的模式。
AAA模式
排列、行动和断言模式是一种用于编写和组织单元测试的常用策略。它的工作方式如下:
- 在排列阶段,测试所需的所有对象和变量都被设置。
- 接下来,在Act阶段,被测试的函数/方法/类被调用。
- 最后,在Assert阶段,我们验证测试的结果。
这个策略提供了一个简洁的方法来组织单元测试,将测试的所有主要部分分开:设置、执行和验证。另外,单元测试更容易阅读,因为它们都遵循相同的结构。
Python中的单元测试:unittest或pytest?
现在我们来谈谈Python中两个不同的单元测试框架。这两个框架是unittest 和pytest 。
unittest简介
Python 标准库包括unittest单元测试框架。这个框架受到 JUnit 的启发,它是 Java 中的一个单元测试框架。
正如官方文档中所说,unittest 支持一些重要的概念,我们将在本文中提及:
- 测试用例,这是测试的单一单元
- 测试套件,它是一组一起执行的测试案例
- 测试运行器,它是处理所有测试用例的执行和结果的组件
unittest 有它的方式来编写测试。特别是,我们需要。
- 将我们的测试写成一个子类的方法
unittest.TestCase - 使用特殊的断言方法
由于unittest 已经安装好了,我们就可以写我们的第一个单元测试了!
使用 unittest 编写单元测试
假设我们有BankAccount 这个类。
import unittest
class BankAccount:
def __init__(self, id):
self.id = id
self.balance = 0
def withdraw(self, amount):
if self.balance >= amount:
self.balance -= amount
return True
return False
def deposit(self, amount):
self.balance += amount
return True
我们不能提取比存款可用性更多的钱,所以让我们测试一下这种情况是否被我们的源代码正确处理。
在同一个Python文件中,我们可以添加以下代码。
class TestBankOperations(unittest.TestCase):
def test_insufficient_deposit(self):
# Arrange
a = BankAccount(1)
a.deposit(100)
# Act
outcome = a.withdraw(200)
# Assert
self.assertFalse(outcome)
我们正在创建一个名为TestBankOperations 的类,它是unittest.TestCase 的子类。 这样,我们就创建了一个新的测试案例。
在这个类里面,我们定义了一个单一的测试函数,其方法以test 开始。这很重要,因为每个测试方法都必须以test 开始。
我们希望这个测试方法能够返回False ,这意味着操作失败。为了断言这个结果,我们使用一个特殊的断言方法,叫做assertFalse() 。
我们已经准备好执行测试了。让我们在命令行上运行这个命令。
python -m unittest example.py
这里,example.py 是包含所有源代码的文件的名称。输出结果应该是这样的。
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
很好!这意味着我们的测试是成功的。现在让我们看看,当出现失败时,输出是怎样的。我们在之前的类中添加一个新的测试。让我们试着存入一个负数的钱,当然这是不可能的。我们的代码能处理这种情况吗?
这就是我们的新测试方法。
def test_negative_deposit(self):
# Arrange
a = BankAccount(1)
# Act
outcome = a.deposit(-100)
# Assert
self.assertFalse(outcome)
我们可以使用unittest 的verbose模式来执行这个测试,把-v 标志。
python -m unittest -v example.py
而现在的输出是不同的。
test_insufficient_deposit (example.TestBankOperations) ... ok
test_negative_deposit (example.TestBankOperations) ... FAIL
======================================================================
FAIL: test_negative_deposit (example.TestBankOperations)
----------------------------------------------------------------------
Traceback (most recent call last):
File "example.py", line 35, in test_negative_deposit
self.assertFalse(outcome)
AssertionError: True is not false
----------------------------------------------------------------------
Ran 2 tests in 0.002s
FAILED (failures=1)
在这种情况下,verbose标志给了我们更多的信息。我们知道,test_negative_deposit 失败了。特别是,AssertionError 告诉我们,预期的结果应该是false ,但True is not false ,这意味着该方法返回了True 。
unittest 框架根据我们的需要,提供了不同的断言方法。
assertEqual(x,y),它测试是否x == yassertRaises(exception_type), 测试是否引发了一个特定的异常assertIsNone(x), 测试是否x is NoneassertIn(x,y), 测试是否x in y
现在我们对如何使用unittest 框架编写单元测试有了基本的了解,让我们来看看另一个叫做pytest 的 Python 框架。
pytest简介
pytest 框架是一个Python单元测试框架,它有几个相关的特点。
- 它允许使用较少的代码进行复杂的测试
- 它支持
unittest测试套件 - 它提供超过800个外部插件
由于pytest 默认没有安装,我们必须先安装它。注意,pytest 需要Python 3.7以上。
安装pytest
安装pytest 是很容易的。你只需要运行这个命令。
pip install -U pytest
然后通过输入这个来检查所有的东西是否已经正确安装。
pytest --version
输出结果应该是这样的。
pytest 7.1.2
很好!让我们用pytest 来写第一个测试。
使用pytest编写单元测试
我们将使用之前写的BankAccount 类,我们将测试与之前一样的方法。这样,就更容易比较使用这两个框架编写测试所需的努力了。
为了用pytest 测试,我们需要。
- 创建一个目录,把我们的测试文件放在里面。
- 将我们的测试写在名字以
test_开始或以_test.py结束的文件中。pytest将在当前目录及其子目录中寻找这些文件。
因此,我们创建一个名为test_bank.py 的文件,并把它放到一个文件夹中。这就是我们第一个测试函数的样子。
def test_insufficient_deposit():
# Arrange
a = BankAccount(1)
a.deposit(100)
# Act
outcome = a.withdraw(200)
# Assert
assert outcome == False
正如你已经注意到的,与unittest 版本相比,唯一改变的是断言部分。在这里我们使用普通的 Python 断言方法。
现在我们可以看一下test_bank.py 文件了。
class BankAccount:
def __init__(self, id):
self.id = id
self.balance = 0
def withdraw(self, amount):
if self.balance >= amount:
self.balance -= amount
return True
return False
def deposit(self, amount):
self.balance += amount
return True
def test_insufficient_deposit():
# Arrange
a = BankAccount(1)
a.deposit(100)
# Act
outcome = a.withdraw(200)
# Assert
assert outcome == False
为了运行这个测试,让我们在test_bank.py 文件所在的文件夹中打开一个命令提示符。然后,运行这个。
pytest
输出结果将是这样的。
======== test session starts ========
platform win32 -- Python 3.7.11, pytest-7.1.2, pluggy-0.13.1
rootdir: \folder
plugins: anyio-2.2.0
collected 1 item
test_bank.py . [100%]
======== 1 passed in 0.02s ========
在这种情况下,我们可以看到编写和执行一个测试是多么容易。另外,我们可以看到,与unittest 相比,我们写的代码更少。测试的结果也很容易理解。
让我们继续看一个失败的测试!
我们使用之前写的第二个方法,它被称为test_negative_deposit 。我们重构了断言部分,结果是这样的。
def test_negative_deposit():
# Arrange
a = BankAccount(1)
# Act
outcome = a.deposit(-100)
# Assert
assert outcome == False
我们以之前的方式运行测试,这应该是输出结果。
======= test session starts =======
platform win32 -- Python 3.7.11, pytest-7.1.2, pluggy-0.13.1
rootdir: \folder
plugins: anyio-2.2.0
collected 2 items
test_bank.py .F [100%]
======= FAILURES =======
_____________ test_negative_deposit _____________
def test_negative_deposit():
# Arrange
a = BankAccount(1)
# Act
outcome = a.deposit(-100)
# Assert
> assert outcome == False
E assert True == False
test_bank.py:32: AssertionError
======= short test summary info =======
FAILED test_bank.py::test_negative_deposit - assert True == False
======= 1 failed, 1 passed in 0.15s =======
通过解析输出,我们可以读到collected 2 items ,这意味着两个测试已经被执行。向下滚动,我们可以读到,在测试test_negative_deposit 方法时发生了故障。特别是,在评估断言时发生了错误。另外,报告还说,outcome 变量的值是True ,所以这意味着deposit 方法包含一个错误。
由于pytest 使用了默认的 Python 断言关键字,我们可以将得到的任何输出与另一个存储预期结果的变量进行比较。所有这些都不需要使用特殊的断言方法。
总结
总结一下,在这篇文章中我们涵盖了软件测试的基础知识。我们发现为什么软件测试是必不可少的,为什么每个人都应该测试他们的代码。我们谈到了单元测试,以及如何在Python中设计和实现简单的单元测试。
我们使用了两个Python框架,叫做unittest 和pytest 。两者都有有用的功能,它们是Python单元测试中最常用的两个框架。
最后,我们看到了两个基本的测试案例,让你了解如何按照Arrange、Act和Assert模式编写测试。
我希望我已经让你相信了软件测试的重要性。选择一个框架,如unittest 或pytest ,并开始测试--因为这值得付出额外的努力!