Python 单元测试

发布时间:2022-07-24

在本教程中,我们将使用 Python 实现单元测试。使用 Python 进行单元测试本身就是一个巨大的话题,但是我们将讨论几个基本概念。

什么是 Python 单元测试?

单元测试是一种由开发人员自己测试特定模块来检查是否有错误的技术。单元测试的主要重点是测试系统的单个单元,以分析、检测和修复错误。 Python 提供单元测试模块来测试源代码的单元。当我们编写庞大的代码时,unittest 起着至关重要的作用,它提供了检查输出是否正确的工具。 通常,我们打印该值并将其与参考输出进行匹配,或者手动检查输出。这个过程需要很多时间。为了克服这个问题,Python 引入了 unittest 模块。我们还可以通过使用它来检查应用的性能。 我们将学习如何创建一个基本测试,发现错误,并在代码交付给用户之前执行它。

测试代码

我们可以用许多方法测试我们的代码。在本节中,我们将学习高级方法的基本步骤。

自动化测试与手动测试

人工测试还有另一种形式,称为探索性测试。这是一次没有任何计划的测试。要进行手动测试,我们需要准备一份应用列表;我们输入不同的输入,等待预期的输出。 每次我们给出输入或更改代码时,我们都需要仔细检查列表中的每一个特性并进行检查。这是最常见的测试方式,也是一个耗时的过程。 另一方面,自动化测试根据我们的代码计划执行代码,这意味着它运行我们想要测试的代码的一部分,即我们想要通过脚本而不是人来测试它们的顺序。 Python 提供了一套工具和库来帮助我们为应用创建自动化测试。

单元测试与集成测试

假设我们想检查汽车的灯光,以及如何测试它们。我们会打开灯走到车外,或者问朋友灯是否亮着。开灯会认为是测试步骤,到外面或者问朋友会知道是测试断言。在集成测试中,我们可以一次测试多个组件。 这些组件可以是我们代码中的任何东西,例如我们编写的函数、类和模块。 但是集成测试有一个局限性;如果集成测试没有给出预期的结果怎么办。在这种情况下,将很难识别系统的哪个部分正在下降。我们举前面的例子;如果灯没亮,电池可能没电了,灯泡坏了,汽车的电脑有故障。 这就是为什么我们考虑单元测试来了解测试代码中的确切问题。 单元测试是一个较小的测试,它检查单个组件是否以正确的方式工作。使用单元测试,我们可以分离出哪些必需品需要在我们的系统中固定。 到目前为止,我们已经看到了两种类型的测试;集成测试检查多个组件;其中单元测试检查应用中的小组件。 让我们理解下面的例子。 我们将单元测试 Python 内置函数 sum() 应用于已知输出。我们检查数字 (2, 3, 5)sum() 等于 10

assert sum([2, 3, 5]) == 10, "Should be 10"

上面一行将返回正确的结果,因为值是正确的。如果我们传递错误的参数,它将返回断言错误。例如:

assert sum([1, 3, 5]) == 10, "Should be 10"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: Should be 10

我们可以将上述代码放入文件中,并在命令行中再次执行。

def test_sum():
    assert sum([2, 3, 5]) == 10, "It should be 10"
if __name__ == "__main__":
    test_sum()
    print("Everything passed")

输出:

$ python sum.py
Everything is correct

在下面的例子中,为了测试的目的,我们将传递元组。创建名为 test_sum2.py 的新文件。 示例- 2:

def test_sum2():
    assert sum([2, 3, 5]) == 10, "It should be 10"
def test_sum_tuple():
    assert sum((1, 3, 5)) == 10, "It should be 10"
if __name__ == "__main__":
    test_sum2()
    test_sum_tuple()
    print("Everything is correct")

输出:

Everything is correct
Traceback (most recent call last):
  File "<string>", line 13, in <module>
File "<string>", line 9, in test_sum_tuple
AssertionError: It should be 10

解释: 在上面的代码中,我们将错误的输入传递给了 test_sum_tuple()。输出与预测结果不同。 上面的方法很好,但是如果有多个错误呢?如果遇到第一个错误,Python 解释器会立即给出一个错误。为了解决这个问题,我们使用了测试运行器。

选择测试运行程序

Python 包含许多测试运行程序。最流行的内置 Python 库叫做 unittestunittest 可移植到其他框架。考虑以下三个最热门的测试跑步者。

  • 单元测试
  • 鼻子还是鼻子 2
  • pytest(测试) 我们可以根据自己的要求任意选择。先简单介绍一下。

单元测试

unittest 从 2.1 开始就内置在 Python 标准库中。unittest 最好的一点是,它既有一个测试框架,也有一个测试运行器。unittest 对编写和执行代码没有什么要求。

  • 代码必须使用类和函数编写。
  • 除了内置断言语句之外,测试用例类中不同断言方法的序列。 让我们使用 unittest 用例来实现上面的例子。 示例:
import unittest
class TestingSum(unittest.TestCase):
    def test_sum(self):
        self.assertEqual(sum([2, 3, 5]), 10, "It should be 10")
    def test_sum_tuple(self):
        self.assertEqual(sum((1, 3, 5)), 10, "It should be 10")
if __name__ == '__main__':
    unittest.main()

输出:

.F
-
FAIL: test_sum_tuple (__main__.TestingSum)
--
Traceback (most recent call last):
  File "<string>", line 11, in test_sum_tuple
AssertionError: 9 != 10 : It should be 10
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
Traceback (most recent call last):
  File "<string>", line 14, in <module>
  File "/usr/lib/python3.8/unittest/main.py", line 101, in __init__
    self.runTests()
  File "/usr/lib/python3.8/unittest/main.py", line 273, in runTests
    sys.exit(not self.result.wasSuccessful())
SystemExit: True

正如我们在输出中看到的,它显示了点(.)执行成功,F 执行失败。

鼻子

有时,我们需要为应用编写数百或数千个测试行;这变得很难理解。 nose 测试运行器可以是 unittest 测试运行器的合适替代,因为它与使用 unittest 框架编写的任何测试兼容。鼻子有两种——鼻子和鼻头。我们建议使用 nose2,因为它是最新版本。 使用 nose2,我们需要使用以下命令安装它。

pip install nose2

在终端中运行以下命令,使用 nose2 测试代码。

python -m nose2

输出如下。

FAIL: test_sum_tuple (__main__.TestSum)
--
Traceback (most recent call last):
  File "test_sum_unittest.py", line 10, in test_sum_tuple
    self.assertEqual(sum((2, 3, 5)), 10, "It should be 10")
AssertionError: It should be 10
--
Ran 2 tests in 0.001s
FAILED (failures=1)

nose2 提供了许多命令行标志来过滤测试。您可以从其官方文档中了解更多信息。

pytest(测试)

pytest 测试运行器支持执行 unittest 测试用例。pytest 的实际好处是编写 pytest 测试用例。pytest 测试用例通常是从 Python 文件开始的方法序列。 pytest 提供以下好处:

  • 它支持内置的 assert 语句,而不是使用特殊的 assert 方法。
  • 它还为测试用例的清理提供支持。
  • 它可以从最后的案例中重新运行。
  • 它有一个由数百个插件组成的生态系统来扩展功能。 让我们理解下面的例子。 示例:
def test_sum():
    assert sum([2, 3, 5]) == 10, "It should be 10"
def test_sum_tuple():
    assert sum((1, 2, 5)) == 10, "It should be 10"

写初试

在这里,我们将应用我们在前面部分学到的所有概念。首先,我们需要创建一个文件名 test.py 或者任何东西。然后输入并执行被测试的代码,捕获输出。成功运行代码后,将输出与预期结果进行匹配。 首先,我们创建文件 my_sum 文件,并在其中编写代码。

def sum(arg):
    total = 0
    for val in arg:
        total += val
    return total

我们初始化了 total 变量,该变量迭代 arg 中的所有值。 现在,我们用下面的代码创建一个文件名 test.py。 示例:

import unittest
from my_sum import sum
class CheckSum(unittest.TestCase):
    def test_list_int(self):
        data = [1, 2, 3]
        result = sum(data)
        self.assertEqual(result, 6)
if __name__ == '__main__':
    unittest.main()

输出:

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

说明: 在上面的代码中,我们从我们创建的 my_sum 包中导入了 sum()。我们定义了检查类,继承了单元测试用例。有一个测试方法 test_list_int(),测试整数。 运行代码后,返回点(.)表示代码没有错误。 让我们理解另一个例子。 示例- 2

class Person:
    name1 = []
    def set_name(self, user_name):
        self.name1.append(user_name)
        return len(self.name1) - 1
    def get_name(self, user_id):
        if user_id >= len(self.name1):
            return ' No such user Find'
        else:
            return self.name1[user_id]
if __name__ == '__main__':
    person = Person()
    print('Peter Decosta has been added with id ', person.set_name('Peter'))
    print('The user associated with id 0 is ', person.get_name(0))

输出:

Peter Decosta has been added with id 0
The user associated with id 0 is Peter

Python 基本函数和单元测试输出

unittest 模块产生三种可能的结果。以下是潜在的结果。

  1. OK - 如果所有测试都通过,则返回 OK
  2. 失败 - 如果任何测试失败,它将引发评估错误异常。
  3. 错误 - 如果出现任何错误而不是断言错误。 让我们看看下面的基本功能。 | 方法 | 描述 | | --- | --- | | assertEqual(a, b) | a == b | | assertTrue(x) | 布尔(x)为真 | | assertFalse(x) | 布尔(x)为假 | | assertIs(a, b) | ab | | assertNone(x) | xNone | | assertIn(a, b) | ab 中 | | assertIsInstance(a, b) | isinstance(a, b) | | assertNotIn(a, b) | a 不在 b | | assertNotIsInstance(a, b) | 非 isinstance(a, b) | | assertIsNot(a, b) | a 不是 b |

Python 单元测试示例

import unittest
# First we import the class which we want to test.
import Person1 as PerClass
class Test(unittest.TestCase):
    """
    The basic class that inherits unittest.TestCase
    """
    person = PerClass.Person()  # instantiate the Person Class
    user_id = []  # This variable stores the obtained user_id
    user_name = []  # This variable stores the person name
    # It is a test case function to check the Person.set_name function
    def test_0_set_name(self):
        print("Start set_name test\n")
        for i in range(4):
            # initialize a name
            name = 'name' + str(i)
            # put the name into the list variable
            self.user_name.append(name)
            # extraxt the user id obtained from the function
            user_id = self.person.set_name(name)
            # check if the obtained user id is null or not
            self.assertIsNotNone(user_id)
            # store the user id to the list
            self.user_id.append(user_id)
        print("The length of user_id is = ", len(self.user_id))
        print(self.user_id)
        print("The length of user_name is = ", len(self.user_name))
        print(self.user_name)
        print("\nFinish set_name test\n")
    # Second test case function to check the Person.get_name function
    def test_1_get_name(self):
        print("\nStart get_name test\n")
        # total number of stored user information
        length = len(self.user_id)
        print("The length of user_id is = ", length)
        print("The lenght of user_name is = ", len(self.user_name))
        for i in range(6):
            # if i not exceed total length then verify the returned name
            if i < length:
                # if the two name not matches it will fail the test case
                self.assertEqual(self.user_name[i], self.person.get_name(self.user_id[i]))
            else:
                print("Testing for get_name no user test")
                # if length exceeds then check the 'no such user' type message
                self.assertEqual('There is no such user', self.person.get_name(i))
        print("\nFinish get_name test\n")
if __name__ == '__main__':
    # begin the unittest.main()
    unittest.main()

输出:

Start set_name test
The length of user_id is =  4
[0, 1, 2, 3]
The length of user_name is =  4
['name0', 'name1', 'name2', 'name3']
Finish set_name test
Start get_name test
The length of user_id is =  4
The lenght of user_name is =  4
Testing for get_name no user test
.F
======================================================================
FAIL: test_1_get_name (__main__.Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:/Users/DEVANSH SHARMA/PycharmProjects/Hello/multiprocessing.py", line 502, in test_1_get_name
    self.assertEqual('There is no such user', self.person.get_name(i))
AssertionError: 'There is no such user' != ' No such user Find'
- There is no such user
+  No such user Find
----------------------------------------------------------------------
Ran 2 tests in 0.002s
FAILED (failures=1)

高级测试场景

在为应用创建测试时,我们必须遵循给定的步骤。

  • 生成必要的输入
  • 执行代码,获取输出。
  • 将输出与预期结果匹配。 为字符串或数字等输入创建静态值等输入是一项稍微复杂的任务。有时,我们需要创建一个类或上下文的实例。 我们创建的输入数据被称为夹具。我们可以在应用中重用夹具。 当我们重复运行代码并每次传递不同的值并期望相同的结果时,这个过程被称为参数化。

处理预期故障

在前面的例子中,我们通过整数来测试sum();如果我们传递不好的值,比如单个整数或者字符串,会发生什么? sum() 会如预期的那样抛出一个错误。这可能是因为测试失败。 我们可以使用 assertRaises() 来处理预期的错误。在里面用语句。让我们理解下面的例子。 示例:

import unittest
from my_sum import sum
class CheckSum(unittest.TestCase):
    def test_list_int(self):
        # Test that it can sum a list of integers
        data = [1, 2, 3]
        res = sum(data)
        self.assertEqual(res, 6)
    def test_bad_type(self):
        data = "Apple"
        with self.assertRaises(TypeError):
            res = sum(data)
if __name__ == '__main__':
    unittest.main()

输出:

..
----------------------------------------------------------------------
Ran 2 tests in 0.006s
OK

Python Unittest 跳过测试

我们可以使用跳过测试技术跳过单个测试方法或测试用例。在测试结果中,失败不算失败。 考虑下面的示例,无条件地跳过该方法。 示例:

import unittest
def add(x,y):
    c = x + y
    return c
class SimpleTest(unittest.TestCase):
   @unittest.skip("The example skipping method")
   def testadd1(self):
      self.assertEquals(add(10,5),7)
if __name__ == '__main__':
   unittest.main()

输出:

s
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK (skipped=1)

说明: 在上例中, skip() 方法以@ token 为前缀。它将一个参数作为日志消息,我们可以在其中描述跳过的原因。的字符表示测试已成功跳过。 我们可以根据特定的条件跳过特定的方法或块。 示例- 2:

import unittest
class suiteTest(unittest.TestCase):
    a = 100
    b = 40
    def test_add(self):
        res = self.a + self.b
        self.assertEqual(res, 100)
    @unittest.skipIf(a > b, "Skip because a is greater than b")
    def test_sub(self):
        res = self.a - self.b
        self.assertTrue(res == -10)
    @unittest.skipUnless(b == 0, "Skip because b is equal to zero")
    def test_div(self):
        res = self.a / self.b
        self.assertTrue(res == 1)
    @unittest.expectedFailure
    def test_mul(self):
        res = self.a * self.b
        self.assertEqual(res == 0)
if __name__ == '__main__':
    unittest.main()

输出:

Fsx.
======================================================================
FAIL: test_add (__main__.suiteTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:/Users/DEVANSH SHARMA/PycharmProjects/Hello/multiprocessing.py", line 539, in test_add
    self.assertEqual(res, 100)
AssertionError: 50 != 100
----------------------------------------------------------------------
Ran 4 tests in 0.001s
FAILED (failures=1, skipped=1, expected failures=1)

说明: 正如我们在输出中看到的,条件 b == 0a > b 为真,因此 test_mul() 方法已跳过。另一方面, test_mul 被标记为预期失败。

结论

我们已经讨论了与 Python 单元测试相关的非常重要的概念。作为初学者,我们需要编写智能的、可维护的方法来验证我们的代码。一旦我们在 Python 单元测试中获得了一个合适的命令,我们就可以切换到其他框架,比如pytest,并利用更高级的特性。