Python单元测试

Python单元测试

在这个教程中,我们将使用Python实现单元测试。使用Python进行单元测试本身就是一个庞大的主题,但是我们将涵盖一些基本概念。

什么是Python unittest

单元测试是一种技术,开发人员可以通过测试特定的模块来检查是否有任何错误。单元测试的主要重点是测试系统的单个单元,分析、检测和修复错误。

Python提供了 unittest模块 来测试源代码的单元。当我们编写大量代码时,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库称为 unittest。 unittest对其他框架也是可移植的。考虑以下三个最流行的测试运行器。

  • unittest
  • nose或nose2
  • pytest

我们可以根据自己的需求选择其中任何一个。让我们简要介绍一下。

unittest

unittest自从2.1版本以来就内置在Python标准库中。unittest最好的地方在于它同时提供了一个测试框架和一个测试运行器。编写和执行代码有以下几个unittest的要求。

  • 代码必须使用类和函数进行编写。
  • TestCase 类中除了内置的断言语句之外,还必须有一系列不同的断言函数。

让我们使用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

有时,我们需要为应用程序编写数百或数千行测试代码,这变得非常困难。

nose测试运行器可以作为unittest测试运行器的适当替代,因为它与使用unittest框架编写的任何测试兼容。nose有两种类型 – nose和nose2。我们建议使用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

我们初始化了总变量,它遍历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() 。我们定义了一个 Checkclass ,它继承自 unittest.TestCase 。有一个测试方法 .test_list_int() ,用于测试整数。

运行代码后,它返回 dot(.) ,这意味着代码没有错误。

让我们理解另一个示例。

示例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. 失败 – 如果任何一个测试失败,它会引发一个 AssertionError 异常。
  3. 错误 – 如果发生任何错误,而不是断言错误。

让我们看一下以下基本函数。

方法 描述
.assertEqual(a, b) a b
.assertTrue(x) bool(x) is True
.assertFalse(x) bool(x) is False
.assertIs(a, b) a is b
.assertIsNone(x) x is None
.assertIn(a, b) a in b
.assertIsInstance(a, b) isinstance(a, b)
.assertNotIn(a, b) a not in b
.assertNotIsInstance(a,b) not isinstance(a, b)
.assertIsNot(a, b) a is not 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()来处理预期的错误。它被用在with语句内部。让我们来看下面的示例。

示例

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 跳过测试

我们可以使用跳过测试技术来跳过单个测试方法或 TestCase 。失败不会被计为TestResult中的一个失败。

考虑以下示例来无条件地跳过方法。

示例

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)

解释:

在上面的示例中,@token前缀的skip()方法。它接受一个日志消息的参数,我们可以在其中描述跳过的原因。s字符表示成功跳过了一个测试。

我们可以根据特定的条件跳过特定的方法或块。

示例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 eqaul 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 == 0和a>b为真,所以test_mul()方法被跳过了。另一方面,test_mul已被标记为预期失败。

结论

我们已经讨论了与Python单元测试相关的重要概念。作为初学者,我们需要编写智能、易于维护的方法来验证我们的代码。一旦我们对Python单元测试有了较好的掌握,就可以切换到其他框架,如pytest,并利用更高级的功能。

Camera课程

Python教程

Java教程

Web教程

数据库教程

图形图像教程

办公软件教程

Linux教程

计算机教程

大数据教程

开发工具教程