用unittest和pytest进行Python单元测试的介绍

204 阅读13分钟

用unittest和pytest进行Python单元测试的介绍

在这篇文章中,我们将看看什么是软件测试,以及为什么你应该关心它。我们将学习如何设计单元测试以及如何编写Python单元测试。特别是,我们将看看Python中两个最常用的单元测试框架,unittestpytest

软件测试简介

软件测试是检查软件产品行为的过程,以评估和验证它是否与规范相一致。软件产品可能有数千行代码,以及数百个一起工作的组件。如果有一行不能正常工作,这个错误就会传播并导致其他错误。因此,为了确保一个程序按照它应该有的方式运行,它必须被测试。

由于现代软件可能相当复杂,所以有多个层次的测试来评估正确性的不同方面。正如ISTQB认证测试基础水平大纲所指出的,软件测试有四个层次:

  1. 单元测试,测试特定的代码行
  2. 集成测试,测试许多单元之间的集成。
  3. 系统测试,测试整个系统
  4. 验收测试,检查是否符合商业目标。

在这篇文章中,我们将讨论单元测试,但在我们深入探讨之前,我想介绍一下软件测试的一个重要原则。

测试显示的是缺陷的存在,而不是缺陷的缺失。

-2018年ISTQB CTFL考试大纲

换句话说,即使你运行的所有测试都没有显示出任何故障,这也不能证明你的软件系统是没有缺陷的,也不能证明另一个测试用例不会在你的软件的行为中发现缺陷。

什么是单元测试?

这是测试的第一个层次,也叫组件测试。在这一部分,对单一的软件组件进行测试。根据编程语言的不同,软件单元可能是一个类,一个函数,或一个方法。例如,如果你有一个叫ArithmeticOperations 的Java类,它有multiplydivide 方法,那么ArithmeticOperations 类的单元测试将需要同时测试multiplydivide 方法的正确行为。

单元测试通常由软件测试人员执行。为了运行单元测试,软件测试人员(或开发人员)需要访问源代码,因为源代码本身就是被测对象。由于这个原因,这种直接测试源代码的软件测试方法被称为白盒测试

你可能想知道为什么你要担心软件测试,以及它是否值得。在下一节,我们将分析测试你的软件系统背后的动机。

为什么你应该做单元测试

软件测试的主要优点是它能提高软件质量。软件质量是至关重要的,特别是在一个软件处理我们各种各样的日常活动的世界中。提高软件的质量仍然是一个太模糊的目标。让我们试着更好地说明我们所说的软件质量是什么意思。根据ISO/IEC标准9126-1ISO 9126,软件质量包括这些因素。

  • 可靠性
  • 功能
  • 效率
  • 可用性
  • 可维护性
  • 可移植性

如果你拥有一家公司,软件测试是你应该仔细考虑的一项活动,因为它可能会对你的业务产生影响。例如,在2022年5月,由于车辆的信息娱乐系统出现问题,特斯拉召回了13万辆汽车。这个问题随后通过 "空中 "分发的软件更新得到修复。这些故障花费了公司的时间和金钱,也给客户带来了问题,因为他们有一段时间无法使用他们的汽车。测试软件确实要花钱,但公司也确实可以在技术支持方面节省数百万美元。

单元测试的重点是检查软件的行为是否正确,这意味着检查输入和输出之间的映射是否都正确。作为一个低级别的测试活动,单元测试有助于早期识别错误,以便它们不会被传播到软件系统的更高层次。

单元测试的其他优点包括。

  • 简化集成:通过确保所有组件单独工作良好,更容易解决集成问题。
  • 尽量减少代码回归:有了大量的测试用例,如果将来对源代码的一些修改会导致问题,那么就更容易定位问题了。
  • 提供文档:通过测试输入和输出之间的正确映射,单元测试提供了关于被测方法或类如何工作的文档。

设计一个测试策略

现在让我们看看如何设计一个测试策略。

测试范围的定义

在开始计划一个测试策略之前,有一个重要的问题需要回答。你想测试你的软件系统的哪些部分?

这是一个关键问题,因为详尽的测试是不可能的。出于这个原因,你不可能测试每一个可能的输入和输出,但你应该根据所涉及的风险来确定测试的优先次序。

在定义你的测试范围时,需要考虑到许多因素。

  • 风险:如果一个错误影响到这个组件,会有什么商业后果?
  • 时间:你希望你的软件产品多长时间能准备好?你有一个最后期限吗?
  • 预算:你愿意在测试活动中投入多少钱?

一旦你定义了测试范围,即规定了你应该测试什么,不应该测试什么,你就可以谈一谈一个好的单元测试应该具备的品质。

单元测试的质量

  • 快速:单元测试大多是自动执行的,这意味着它们必须是快速的。缓慢的单元测试更有可能被开发人员跳过,因为它们不能提供即时反馈。
  • 孤立的:根据定义,单元测试是独立的。他们测试单独的代码单元,他们不依赖于任何外部事物(如文件或网络资源)。
  • 可重复的:单元测试是重复执行的,其结果必须在一段时间内保持一致。
  • 可靠的:单元测试只有在被测系统中存在错误时才会失败。环境或测试的执行顺序不应该有问题。
  • 命名正确:测试的名称应该提供关于测试本身的相关信息。

在深入研究Python的单元测试之前,还缺少最后一步。我们如何组织我们的测试,使其整洁并易于阅读?我们使用一种叫做Arrange, Act and Assert(AAA)的模式。

AAA模式

排列、行动和断言模式是一种用于编写和组织单元测试的常用策略。它的工作方式如下:

  • 排列阶段,测试所需的所有对象和变量都被设置。
  • 接下来,在Act阶段,被测试的函数/方法/类被调用。
  • 最后,在Assert阶段,我们验证测试的结果。

这个策略提供了一个简洁的方法来组织单元测试,将测试的所有主要部分分开:设置、执行和验证。另外,单元测试更容易阅读,因为它们都遵循相同的结构。

Python中的单元测试:unittest或pytest?

现在我们来谈谈Python中两个不同的单元测试框架。这两个框架是unittestpytest

unittest简介

Python 标准库包括unittest单元测试框架。这个框架受到 JUnit 的启发,它是 Java 中的一个单元测试框架。

正如官方文档中所说,unittest 支持一些重要的概念,我们将在本文中提及:

  • 测试用例,这是测试的单一单元
  • 测试套件,它是一组一起执行的测试案例
  • 测试运行器,它是处理所有测试用例的执行和结果的组件

unittest 有它的方式来编写测试。特别是,我们需要。

  1. 将我们的测试写成一个子类的方法unittest.TestCase
  2. 使用特殊的断言方法

由于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 == y
  • assertRaises(exception_type), 测试是否引发了一个特定的异常
  • assertIsNone(x), 测试是否x is None
  • assertIn(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框架,叫做unittestpytest 。两者都有有用的功能,它们是Python单元测试中最常用的两个框架。

最后,我们看到了两个基本的测试案例,让你了解如何按照Arrange、Act和Assert模式编写测试。

我希望我已经让你相信了软件测试的重要性。选择一个框架,如unittestpytest ,并开始测试--因为这值得付出额外的努力!