Python教程-Python 单元测试
在这个教程中,我们将使用 Python 实现单元测试。使用 Python 进行单元测试本身是一个庞大的主题,但我们将涵盖一些基本概念。
什么是 Python unittest?
Unit testing 是一种技术,开发者使用它来测试特定的模块,以检查是否存在任何错误。单元测试的主要重点是测试系统中的个别单元,以分析、检测和修复错误。
Python 提供了 unittest 模块 来测试源代码的单元。当我们编写大量代码时,unittest 在其中发挥了重要作用,并且它提供了检查输出是否正确的功能。
通常,我们会打印值并将其与参考输出进行比较,或者手动检查输出。
这个过程需要大量时间。为了解决这个问题,Python 引入了 unittest 模块。我们还可以使用它来检查应用程序的性能。
我们将学习如何创建基本测试、查找错误并在代码交付给用户之前执行它。
测试代码
我们可以使用多种方式来测试我们的代码。在这一部分,我们将学习从基本步骤到高级方法的基础知识。
自动化 vs. 手动测试
手动测试还有另一种形式,被称为探索性测试。这是一种在没有计划的情况下进行的测试。为了进行手动测试,我们需要准备一个应用程序的列表;我们输入不同的输入并等待期望的输出。
每次我们提供输入或更改代码时,都需要查看列表的每个单个功能并检查它。
这是最常见的测试方式,但也是耗时的过程。
另一方面,自动化测试按照我们的代码计划执行代码,这意味着它通过脚本而不是人来运行我们想要测试的代码的一部分。
Python 提供了一组工具和库,帮助我们为应用程序创建自动化测试。
单元测试 vs. 集成测试
假设我们想要检查汽车的灯以及如何测试它们。我们会打开灯,走出汽车或询问朋友灯是否亮。打开灯被视为测试步骤,走出汽车或询问朋友则被视为测试断言。在集成测试中,我们可以一次测试多个组件。
这些组件可以是我们的代码中的任何内容,例如我们编写的函数、类和模块。
但集成测试有一个限制;如果集成测试未提供预期的结果,那么很难识别系统的哪一部分失败了。让我们看看前面的例子;如果灯没有亮,那么可能是电池耗尽了、灯泡坏了、汽车的电脑出现故障。
这就是为什么我们考虑使用单元测试来了解受测试代码中的确切问题。
单元测试是一个较小的测试,它检查单个组件是否正常工作。使用单元测试,我们可以确定需要在我们的系统中修复什么。
到目前为止,我们已经看到了两种测试类型;集成测试检查多个组件,而单元测试检查应用程序中的小组件。
让我们理解以下示例。
我们应用单元测试 Python 内置函数 sum() 来测试已知输出。我们检查数字 (2, 3, 5) 的 sum() 是否等于 10。
assert sum([ 2, 3, 5]) == 10, "Should be 10"
上面的代码将返回正确的结果,因为值是正确的。如果我们传递错误的参数,它将返回 Assertion error。例如 -
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 从 Python 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
正如我们在输出中所看到的,对于成功执行,它显示了 dot(.),对于一个失败,它显示了 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() 方法。
- 它还提供了用于测试用例的清理支持。
- 它可以从上次测试用例重新运行。
- 它具有数百个插件的生态系统,以扩展功能。
让我们理解以下示例。
示例 -
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()。我们定义了 CheckSum 类,它继承自 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 模块会生成三种可能的结果。以下是潜在的结果。
- OK - 如果所有测试都通过,则返回 OK。
- Failure - 如果任何一个测试失败,它将引发 AssertionError 异常。
- Error - 如果出现错误而不是断言错误。
让我们看一下以下基本函数。
方法 | 描述 |
---|---|
.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,并利用更高级的功能。