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模块会产生三种可能的结果。以下是可能的结果。
- OK – 如果所有测试都通过,它会返回OK。
- 失败 – 如果任何一个测试失败,它会引发一个 AssertionError 异常。
- 错误 – 如果发生任何错误,而不是断言错误。
让我们看一下以下基本函数。
方法 | 描述 |
---|---|
.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,并利用更高级的功能。