单元测试框架 - 快速指南


单元测试框架 - 概述

单元测试是一种软件测试方法,通过测试源代码的各个单元(例如函数、方法和类)以确定它们是否适合使用。直观上,我们可以将单元视为应用程序的最小可测试部分。单元测试是程序员在开发过程中创建的短代码片段。它构成了组件测试的基础。

单元测试可以通过以下两种方式完成 -

手动测试 自动化测试

在没有任何工具支持的情况下手动执行测试用例称为手动测试。

  • 由于测试用例是由人力资源执行的,因此非常耗时且乏味

  • 由于测试用例需要手动执行,因此需要更多的测试人员进行手动测试。

  • 它不太可靠,因为由于人为错误,每次测试可能无法精确执行。

  • 无法通过编程来编写获取隐藏信息的复杂测试。

借助工具支持并使用自动化工具执行测试用例称为自动化测试。

  • 快速自动化运行测试用例的速度明显快于人力资源。

  • 由于使用自动化工具执行测试用例,因此对人力资源的投资较少。

  • 自动化测试每次运行时都会执行完全相同的操作,并且更加可靠

  • 测试人员可以编写复杂的测试程序来找出隐藏的信息。

JUnit 是 Java 编程语言的单元测试框架。JUnit 在测试驱动开发的发展中发挥着重要作用,是起源于 JUnit 的单元测试框架系列之一,统称为 xUnit。您可以在此处找到JUnit 教程

Python 单元测试框架(有时称为“PyUnit”)是由 Kent Beck 和 Erich Gamma 开发的 JUnit 的 Python 语言版本。自 Python 2.1 版本起,PyUnit 成为 Python 标准库的一部分。

Python 单元测试框架支持测试自动化、共享测试的设置和关闭代码、将测试聚合到集合中以及测试与报告框架的独立性。unittest 模块提供的类可以轻松支持一组测试的这些质量。

本教程是为初学者准备的,帮助他们了解Python测试框架的基本功能。完成本教程后,您将发现自己在使用 Python 测试框架方面拥有中等水平的专业知识,从中您可以将自己提升到新的水平。

您应该具备使用 Python 语言进行软件开发的合理专业知识。我们的Python 教程是开始学习 Python 的好地方。了解软件测试的基础知识也是必要的。

环境设置

编写测试所需的类可以在“unittest”模块中找到。如果您使用旧版本的 Python(Python 2.1 之前的版本),可以从http://pyunit.sourceforge.net/下载该模块。然而,unittest 模块现在是标准 Python 发行版的一部分;因此它不需要单独安装。

单元测试框架 - 框架

“unittest”支持测试自动化、共享测试的设置和关闭代码、将测试聚合到集合中以及测试与报告框架的独立性。

unittest 模块提供的类可以轻松支持一组测试的这些质量。

为了实现这一目标,unittest 支持以下重要概念 -

  • 测试夹具- 这表示执行一项或多项测试以及任何相关清理操作所需的准备工作。例如,这可能涉及创建临时或代理数据库、目录或启动服务器进程。

  • 测试用例- 这是最小的测试单元。这会检查对一组特定输入的特定响应。unittest 提供了一个基类TestCase,可用于创建新的测试用例。

  • 测试套件- 这是测试用例、测试套件或两者的集合。这用于聚合应一起执行的测试。测试套件由 TestSuite 类实现。

  • 测试运行器- 这是一个协调测试执行并向用户提供结果的组件。运行器可以使用图形界面、文本界面或返回特殊值来指示执行测试的结果。

创建单元测试

编写简单的单元测试涉及以下步骤 -

步骤 1 - 在程序中导入单元测试模块。

步骤 2 - 定义要测试的函数。下面的例子中,要测试add()函数。

步骤 3 - 通过子类化 unittest.TestCase 创建一个测试用例。

步骤 4 - 将测试定义为类内的方法。方法名称必须以“test”开头。

步骤 5 - 每个测试都调用 TestCase 类的断言函数。断言有很多种类型。以下示例调用assertEquals() 函数。

步骤 6 -assertEquals() 函数将 add() 函数的结果与 arg2 参数进行比较,如果比较失败,则抛出断言错误。

步骤 7 - 最后,从 unittest 模块调用 main() 方法。

import unittest
def add(x,y):
   return x + y
   
class SimpleTest(unittest.TestCase):
   def testadd1(self):
      self.assertEquals(add(4,5),9)
      
if __name__ == '__main__':
   unittest.main()

步骤 8 - 从命令行运行上述脚本。

C:\Python27>python SimpleTest.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK

步骤 9 - 以下三个可能是测试的可能结果 -

先生编号 消息和描述
1

好的

测试通过。控制台上显示“A”。

2

失败

测试未通过,并引发 AssertionError 异常。控制台上显示“F”。

3

错误

该测试引发除 AssertionError 之外的异常。控制台上显示“E”。

这些结果分别通过“.”、“F”和“E”显示在控制台上。

命令行界面

可以从命令行使用unittest模块来运行单个或多个测试。

python -m unittest test1
python -m unittest test_module.TestClass
python -m unittest test_module.TestClass.test_method

unittest 支持以下命令行选项。有关所有命令行选项的列表,请使用以下命令 -

Python –m unittest -h

先生编号 选项和说明
1

-h,--帮助

显示此消息

2

v,--详细

详细输出

3

-q,--安静

最小输出

4

-f, --failfast

第一次失败时停止

5

-c,--catch

捕获 control-C 并显示结果

6

-b,--缓冲区

在测试运行期间缓冲 stdout 和 stderr

单元测试框架 - API

本章讨论unittest模块中定义的类和方法。该模块有五个主要类。

测试用例类

此类的对象代表最小的可测试单元。它保存测试例程并提供用于准备每个例程以及之后清理的挂钩。

TestCase 类中定义了以下方法 -

先生编号 方法及说明
1

设置()

调用方法来准备测试夹具。在调用测试方法之前立即调用此函数

2

拆除()

调用测试方法并记录结果后立即调用方法。即使测试方法引发异常,也会调用此方法,

3

设置类()

在单个类中的测试运行之前调用的类方法。

4

拆解类()

在单个类中的测试运行后调用的类方法。

5

运行(结果=无)

运行测试,将结果收集到作为result传递的测试结果对象中。

6

跳过测试(原因)

在测试方法或 setUp() 期间调用此函数会跳过当前测试。

7

调试()

运行测试而不收集结果。

8

简短的介绍()

返回测试的一行描述。

固定装置

TestCase 类中可以编写大量测试。这些测试方法可能需要数据库连接、临时文件或其他资源进行初始化。这些被称为固定装置。TestCase 包含一个特殊的钩子来配置和清理测试所需的任何固定装置。要配置灯具,请重写 setUp()。要清理,请重写tearDown()。

在以下示例中,在 TestCase 类中编写了两个测试。他们测试两个值的加法和减法结果。setup() 方法根据每个测试的shortDescription() 初始化参数。Teadown() 方法将在每次测试结束时执行。

import unittest

class simpleTest2(unittest.TestCase):
   def setUp(self):
      self.a = 10
      self.b = 20
      name = self.shortDescription()
      if name == "Add":
         self.a = 10
         self.b = 20
         print name, self.a, self.b
      if name == "sub":
         self.a = 50
         self.b = 60
         print name, self.a, self.b
   def tearDown(self):
      print '\nend of test',self.shortDescription()

   def testadd(self):
      """Add"""
      result = self.a+self.b
      self.assertTrue(result == 100)
   def testsub(self):
      """sub"""
      result = self.a-self.b
      self.assertTrue(result == -10)
      
if __name__ == '__main__':
   unittest.main()

从命令行运行上述代码。它给出以下输出 -

C:\Python27>python test2.py
Add 10 20
F
end of test Add
sub 50 60
end of test sub
.
================================================================
FAIL: testadd (__main__.simpleTest2)
Add
----------------------------------------------------------------------
Traceback (most recent call last):
   File "test2.py", line 21, in testadd
      self.assertTrue(result == 100)
AssertionError: False is not true
----------------------------------------------------------------------
Ran 2 tests in 0.015s

FAILED (failures = 1)

类夹具

TestCase 类有一个 setUpClass() 方法,可以在执行 TestCase 类内的各个测试之前重写该方法。类似地,tearDownClass() 方法将在类中的所有测试之后执行。这两个方法都是类方法。因此,它们必须用 @classmethod 指令修饰。

以下示例演示了这些类方法的用法 -

import unittest

class TestFixtures(unittest.TestCase):

   @classmethod
   def setUpClass(cls):
      print 'called once before any tests in class'

   @classmethod
   def tearDownClass(cls):
      print '\ncalled once after all tests in class'

   def setUp(self):
      self.a = 10
      self.b = 20
      name = self.shortDescription()
      print '\n',name
   def tearDown(self):
      print '\nend of test',self.shortDescription()

   def test1(self):
      """One"""
      result = self.a+self.b
      self.assertTrue(True)
   def test2(self):
      """Two"""
      result = self.a-self.b
      self.assertTrue(False)
      
if __name__ == '__main__':
unittest.main()

测试套件类

Python 的测试框架提供了一种有用的机制,通过该机制可以根据测试用例实例所测试的功能将测试用例实例分组在一起。该机制由unittest模块中的TestSuite类提供。

创建和运行测试套件涉及以下步骤。

步骤 1 - 创建 TestSuite 类的实例。

suite = unittest.TestSuite()

步骤 2 - 在套件的 TestCase 类中添加测试。

suite.addTest(testcase class)

步骤 3 - 您还可以使用 makeSuite() 方法从类中添加测试

suite = unittest.makeSuite(test case class)

步骤 4 - 也可以在套件中添加单独的测试。

suite.addTest(testcaseclass(""testmethod")

步骤 5 - 创建 TestTestRunner 类的对象。

runner = unittest.TextTestRunner()

步骤 6 - 调用 run() 方法来运行套件中的所有测试

runner.run (suite)

TestSuite 类中定义了以下方法 -

先生编号 方法及说明
1

添加测试()

在测试套件中添加测试方法。

2

添加测试()

添加来自多个 TestCase 类的测试。

3

跑步()

运行与该套件关联的测试,将结果收集到测试结果对象中

4

调试()

运行与此套件关联的测试而不收集结果。

5

计数测试用例()

返回此测试对象代表的测试数量

以下示例显示如何使用 TestSuite 类 -

import unittest
class suiteTest(unittest.TestCase):
   def setUp(self):
      self.a = 10
      self.b = 20
      
   def testadd(self):
      """Add"""
      result = self.a+self.b
      self.assertTrue(result == 100)
   def testsub(self):
      """sub"""
      result = self.a-self.b
      self.assertTrue(result == -10)
      
def suite():
   suite = unittest.TestSuite()
##   suite.addTest (simpleTest3("testadd"))
##   suite.addTest (simpleTest3("testsub"))
   suite.addTest(unittest.makeSuite(simpleTest3))
   return suite
   
if __name__ == '__main__':
   runner = unittest.TextTestRunner()
   test_suite = suite()
   runner.run (test_suite)

您可以通过取消注释具有 makeSuite() 方法的行和注释语句来试验 addTest() 方法。

测试加载器类

unittest 包具有 TestLoader 类,用于从类和模块创建测试套件。默认情况下,当调用unittest.main(0方法时,会自动创建unittest.defaultTestLoader实例。但是,显式实例可以自定义某些属性。

在以下代码中,使用 TestLoader 对象将两个类的测试收集到一个 List 中。

import unittest
testList = [Test1, Test2]
testLoad = unittest.TestLoader()

TestList = []
for testCase in testList:
   testSuite = testLoad.loadTestsFromTestCase(testCase)
   TestList.append(testSuite)
   
newSuite = unittest.TestSuite(TestList)
runner = unittest.TextTestRunner()
runner.run(newSuite)

下表显示了 TestLoader 类中的方法列表 -

序列号 方法及说明
1

从测试用例加载测试()

返回 TestCase 类中包含的所有测试用例的套件

2

从模块加载测试()

返回给定模块中包含的所有测试用例的套件。

3

从名称加载测试()

返回一组给定字符串说明符的所有测试用例。

4

发现()

通过从指定起始目录递归到子目录查找所有测试模块,并返回一个 TestSuite 对象

测试结果类

此类用于编译有关已成功的测试和已失败的测试的信息。TestResult 对象存储一组测试的结果。TestRunner.run() 方法返回 TestResult 实例。

TestResult 实例具有以下属性 -

先生编号 属性及描述
1

错误

包含 TestCase 实例的 2 元组和保存格式化回溯的字符串的列表。每个元组代表一个引发意外异常的测试。

2

失败

包含 TestCase 实例的 2 元组和保存格式化回溯的字符串的列表。每个元组代表一个测试,其中使用 TestCase.assert*() 方法显式发出失败信号。

3

跳过

包含 TestCase 实例的 2 元组和包含跳过测试原因的字符串的列表。

4

那是成功的()

如果到目前为止运行的所有测试都已通过,则返回 True,否则返回 False。

5

停止()

可以调用此方法来发出信号,表明正在运行的测试集应该中止。

6

开始测试运行()

在执行任何测试之前调用一次。

7

停止测试运​​行()

所有测试执行后调用一次。

8

测试运行

迄今为止运行的测试总数。

9

缓冲

如果设置为 true,sys.stdoutsys.stderr将在调用 startTest() 和 stopTest() 之间进行缓冲。

以下代码执行测试套件 -

if __name__ == '__main__':
   runner = unittest.TextTestRunner()
   test_suite = suite()
   result = runner.run (test_suite)
   
   print "---- START OF TEST RESULTS"
   print result

   print "result::errors"
   print result.errors

   print "result::failures"
   print result.failures

   print "result::skipped"
   print result.skipped

   print "result::successful"
   print result.wasSuccessful()
   
   print "result::test-run"
   print result.testsRun
   print "---- END OF TEST RESULTS"

执行时的代码显示以下输出 -

---- START OF TEST RESULTS
<unittest.runner.TextTestResult run = 2 errors = 0 failures = 1>
result::errors
[]
result::failures
[(<__main__.suiteTest testMethod = testadd>, 'Traceback (most recent call last):\n
   File "test3.py", line 10, in testadd\n 
   self.assertTrue(result == 100)\nAssert
   ionError: False is not true\n')]
result::skipped
[]
result::successful
False
result::test-run
2
---- END OF TEST RESULTS

单元测试框架 - 断言

Python 测试框架使用Python 内置的assert() 函数来测试特定条件。如果断言失败,将引发 AssertionError。然后测试框架会将测试识别为失败。其他异常均视为错误。

以下三组断言函数在unittest模块中定义 -

  • 基本布尔断言
  • 比较断言
  • 集合断言

基本断言函数评估运算结果是 True 还是 False。所有的断言方法都接受一个msg参数,如果指定的话,将用作失败时的错误消息。

先生编号 方法及说明
1

断言Equal(arg1,arg2,msg =无)

测试arg1arg2是否相等。如果值比较不相等,则测试将失败。

2

断言NotEqual(arg1,arg2,msg =无)

测试arg1arg2是否不相等。如果这些值比较相等,则测试将失败。

3

断言True(表达式,味精=无)

测试expr是否为真。如果为 false,则测试失败

4

断言假(表达式,味精=无)

测试expr是否为假。如果为 true,则测试失败

5

断言Is(arg1,arg2,msg =无)

测试arg1arg2是否计算为同一对象。

6

断言IsNot(arg1,arg2,msg =无)

测试arg1arg2的计算结果是否为同一对象。

7

断言IsNone(表达式,味精=无)

测试expr是否为 None。如果不是 None,则测试失败

8

断言IsNotNone(expr, msg = None)

测试expr不是 None。如果没有,测试失败

9

断言(arg1,arg2,msg =无)

测试arg1是否在arg2中。

10

断言NotIn(arg1,arg2,msg =无)

测试arg1是否不在arg2中。

11

assertIsInstance(obj, cls, msg = None)

测试obj是否是cls的实例

12

assertNotIsInstance(obj, cls, msg = None)

测试obj不是cls的实例

上述一些断言函数在以下代码中实现 -

import unittest

class SimpleTest(unittest.TestCase):
   def test1(self):
      self.assertEqual(4 + 5,9)
   def test2(self):
      self.assertNotEqual(5 * 2,10)
   def test3(self):
      self.assertTrue(4 + 5 == 9,"The result is False")
   def test4(self):
      self.assertTrue(4 + 5 == 10,"assertion fails")
   def test5(self):
      self.assertIn(3,[1,2,3])
   def test6(self):
      self.assertNotIn(3, range(5))

if __name__ == '__main__':
   unittest.main()

上述脚本运行时,test2、test4、test6会显示失败,其他运行成功。

FAIL: test2 (__main__.SimpleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
   File "C:\Python27\SimpleTest.py", line 9, in test2
      self.assertNotEqual(5*2,10)
AssertionError: 10 == 10

FAIL: test4 (__main__.SimpleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
   File "C:\Python27\SimpleTest.py", line 13, in test4
      self.assertTrue(4+5==10,"assertion fails")
AssertionError: assertion fails

FAIL: test6 (__main__.SimpleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
   File "C:\Python27\SimpleTest.py", line 17, in test6
      self.assertNotIn(3, range(5))
AssertionError: 3 unexpectedly found in [0, 1, 2, 3, 4]

----------------------------------------------------------------------            
Ran 6 tests in 0.001s                                                             
                                                                                  
FAILED (failures = 3)

第二组断言函数是比较断言 -

  • 断言AlmostEqual(第一,第二,位置= 7,消息=无,增量=无)

    通过计算差值、四舍五入到给定的小数位数(默认为 7)来测试第一和第二是否近似(不近似相等

  • assertNotAlmostEqual(第一、第二、位置、消息、增量)

    通过计算差值、四舍五入到给定的小数位数(默认为 7)并与零进行比较,测试第一和第二是否近似相等。

    在上述两个函数中,如果提供 delta 而不是地方,则第一个和第二个之间的差必须小于或等于(或大于)delta。

    同时提供 delta 和 place 会引发类型错误。

  • assertGreater(第一,第二,msg = None)

    根据方法名称测试第一个是否大于第二个。如果没有,测试将失败。

  • assertGreaterEqual(第一,第二,msg = None)

    根据方法名称测试第一个是否大于或等于第二个。如果没有,测试将失败

  • assertLess(第一,第二,msg = None)

    根据方法名称测试第一个是否小于第二个。如果没有,测试将失败

  • assertLessEqual(第一,第二,msg = None)

    根据方法名称测试第一个是否小于或等于第二个。如果没有,测试将失败。

  • assertRegexpMatches(文本,正则表达式,msg = None)

    测试正则表达式搜索是否与文本匹配。如果失败,错误消息将包括模式和文本。regexp 可以是正则表达式对象或包含适合re.search()使用的正则表达式的字符串。

  • 断言NotRegexpMatches(文本,正则表达式,msg =无)

    验证正则表达式搜索与text不匹配。失败并显示错误消息,其中包括模式和匹配的文本部分。regexp可以是正则表达式对象或包含适合re.search()使用的正则表达式的字符串。

断言函数在以下示例中实现 -

import unittest
import math
import re

class SimpleTest(unittest.TestCase):
   def test1(self):
      self.assertAlmostEqual(22.0/7,3.14)
   def test2(self):
      self.assertNotAlmostEqual(10.0/3,3)
   def test3(self):
      self.assertGreater(math.pi,3)
   def test4(self):
      self.assertNotRegexpMatches("Tutorials Point (I) Private Limited","Point")

if __name__ == '__main__':
   unittest.main()

上面的脚本将 test1 和 test4 报告为失败。在test1中,22/7的除法不在3.14的7个小数位之内。同样,由于第二个参数与第一个参数中的文本匹配,因此 test4 会导致 AssertionError。

=====================================================FAIL: test1 (__main__.SimpleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
   File "asserttest.py", line 7, in test1
      self.assertAlmostEqual(22.0/7,3.14)
AssertionError: 3.142857142857143 != 3.14 within 7 places
================================================================
FAIL: test4 (__main__.SimpleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
   File "asserttest.py", line 13, in test4
      self.assertNotRegexpMatches("Tutorials Point (I) Private Limited","Point")
AssertionError: Regexp matched: 'Point' matches 'Point' in 'Tutorials Point (I)
Private Limited'
----------------------------------------------------------------------

Ran 4 tests in 0.001s                                                             
                                                                                  
FAILED (failures = 2)

集合断言

这组断言函数旨在与 Python 中的集合数据类型一起使用,例如 List、Tuple、Dictionary 和 Set。

先生编号 方法及说明
1

断言ListEqual(列表1,列表2,msg =无)

测试两个列表是否相等。如果不是,则会构造一条错误消息,仅显示两者之间的差异。

2

assertTupleEqual(元组1,元组2,msg =无)

测试两个元组是否相等。如果不是,则会构造一条错误消息,仅显示两者之间的差异。

3

assertSetEqual (set1, set2, msg = None)

测试两组是否相等。如果不是,则会构建一条错误消息,列出各组之间的差异。

4

assertDictEqual(预期,实际,msg = None)

测试两个字典是否相等。如果不是,则会构建一条错误消息,显示字典中的差异。

以下示例实现了上述方法 -

import unittest

class SimpleTest(unittest.TestCase):
   def test1(self):
      self.assertListEqual([2,3,4], [1,2,3,4,5])
   def test2(self):
      self.assertTupleEqual((1*2,2*2,3*2), (2,4,6))
   def test3(self):
      self.assertDictEqual({1:11,2:22},{3:33,2:22,1:11})

if __name__ == '__main__':
   unittest.main()

在上面的示例中,test1 和 test3 显示 AssertionError。错误消息显示 List 和 Dictionary 对象中的差异。

FAIL: test1 (__main__.SimpleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
   File "asserttest.py", line 5, in test1
      self.assertListEqual([2,3,4], [1,2,3,4,5])
AssertionError: Lists differ: [2, 3, 4] != [1, 2, 3, 4, 5]

First differing element 0:
2
1

Second list contains 2 additional elements.
First extra element 3:
4

- [2, 3, 4]
+ [1, 2, 3, 4, 5]
? +++       +++

FAIL: test3 (__main__.SimpleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
   File "asserttest.py", line 9, in test3
      self.assertDictEqual({1:11,2:22},{3:33,2:22,1:11})
AssertionError: {1: 11, 2: 22} != {1: 11, 2: 22, 3: 33}
- {1: 11, 2: 22}
+ {1: 11, 2: 22, 3: 33}
?              +++++++
                                                                                  
----------------------------------------------------------------------            
Ran 3 tests in 0.001s                                                             
                                                                                  
FAILED (failures = 2)

UnitTest 框架 - 测试发现

TestLoader 类有一个discover() 函数。Python 测试框架使用它来进行简单的测试发现。为了兼容,包含测试的模块和包必须可以从顶级目录导入。

以下是测试发现的基本命令行用法 -

Python –m unittest discover

解释器尝试递归地从当前目录和内部目录加载包含 test 的所有模块。其他命令行选项是 -

先生编号 选项和说明
1

-v, --详细

详细输出

2

-s, --开始目录

目录 开始发现的目录(.默认)

3

-p,--模式

匹配测试文件的模式(默认为 test*.py)

4

-t, --顶级目录

目录 项目的顶级目录(默认为启动目录)

例如,为了发现“tests”目录中名称以“assert”开头的模块中的测试,请使用以下命令行 -

C:\python27>python –m unittest –v –s "c:\test" –p "assert*.py"

测试发现通过导入测试来加载测试。一旦测试发现从您指定的起始目录找到所有测试文件,它会将路径转换为要导入的包名称。

如果您提供起始目录作为包名称而不是目录路径,则 discovery 会假定它导入的位置就是您想要的位置,因此您不会收到警告。

UnitTest 框架 - 跳过测试

自 Python 2.7 起添加了对跳过测试的支持。可以有条件或无条件地跳过单个测试方法或 TestCase 类。该框架允许将某个测试标记为“预期失败”。该测试将“失败”,但在 TestResult 中不会被计为失败。

要无条件跳过方法,可以使用以下unittest.skip()类方法 -

import unittest

   def add(x,y):
      return x+y

class SimpleTest(unittest.TestCase):
   @unittest.skip("demonstrating skipping")
   def testadd1(self):
      self.assertEquals(add(4,5),9)

if __name__ == '__main__':
   unittest.main()

由于skip()是一个类方法,因此它以@标记为前缀。该方法采用一个参数:一条描述跳过原因的日志消息。

执行上述脚本时,控制台上显示以下结果 -

C:\Python27>python skiptest.py
s
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK (skipped = 1)

字符“s”表示测试已被跳过。

跳过测试的替代语法是在测试函数内使用实例方法skipTest()。

def testadd2(self):
   self.skipTest("another method for skipping")
   self.assertTrue(add(4 + 5) == 10)

以下装饰器实现测试跳过和预期失败 -

先生编号 方法及说明
1

单元测试.跳过(原因)

无条件跳过装饰测试。原因应该描述为什么跳过测试。

2

unittest.skipIf(条件,原因)

如果条件为真,则跳过装饰测试。

3

unittest.skipUnless(条件,原因)

除非条件为真,否则跳过装饰测试。

4

单元测试.expectedFailure()

将测试标记为预期失败。如果测试在运行时失败,则该测试不会被视为失败。

以下示例演示了条件跳过和预期失败的使用。

import unittest

class suiteTest(unittest.TestCase):
   a = 50
   b = 40
   
   def testadd(self):
      """Add"""
      result = self.a+self.b
      self.assertEqual(result,100)

   @unittest.skipIf(a>b, "Skip over this routine")
   def testsub(self):
      """sub"""
      result = self.a-self.b
      self.assertTrue(result == -10)
   
   @unittest.skipUnless(b == 0, "Skip over this routine")
   def testdiv(self):
      """div"""
      result = self.a/self.b
      self.assertTrue(result == 1)

   @unittest.expectedFailure
   def testmul(self):
      """mul"""
      result = self.a*self.b
      self.assertEqual(result == 0)

if __name__ == '__main__':
   unittest.main()

在上面的例子中,testsub()和testdiv()将被跳过。在第一种情况下 a>b 为真,而在第二种情况下 b == 0 则不为真。另一方面,testmul() 已被标记为预期失败。

运行上述脚本时,两个跳过的测试显示“s”,预期失败显示为“x”。

C:\Python27>python skiptest.py
Fsxs
================================================================
FAIL: testadd (__main__.suiteTest)
Add
----------------------------------------------------------------------
Traceback (most recent call last):
   File "skiptest.py", line 9, in testadd
      self.assertEqual(result,100)
AssertionError: 90 != 100

----------------------------------------------------------------------
Ran 4 tests in 0.000s

FAILED (failures = 1, skipped = 2, expected failures = 1)

UnitTest 框架 - 异常测试

Python 测试框架提供了以下断言方法来检查是否引发异常。

断言Raises(异常,可调用,*args,**kwds)

测试使用任何位置或关键字参数调用函数时是否引发异常(第一个参数)。如果引发预期的异常,则测试通过;如果引发另一个异常,则测试为错误;如果未引发异常,则测试失败。要捕获一组异常中的任何一个,可以将包含异常类的元组作为异常传递。

在下面的示例中,定义了一个测试函数来检查是否引发 ZeroDivisionError。

import unittest

def div(a,b):
   return a/b
class raiseTest(unittest.TestCase):
   def testraise(self):
      self.assertRaises(ZeroDivisionError, div, 1,0)

if __name__ == '__main__':
   unittest.main()

testraise() 函数使用assertRaises() 函数来查看调用div() 函数时是否发生被零除的情况。上面的代码会引发异常。但将 div() 函数的参数更改如下 -

self.assertRaises(ZeroDivisionError, div, 1,1)

当使用这些更改运行代码时,测试会失败,因为不会发生 ZeroDivisionError。

F
================================================================
FAIL: testraise (__main__.raiseTest)
----------------------------------------------------------------------
Traceback (most recent call last):
   File "raisetest.py", line 7, in testraise
      self.assertRaises(ZeroDivisionError, div, 1,1)
AssertionError: ZeroDivisionError not raised

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures = 1)

assertRaisesRegexp(异常、正则表达式、可调用、*args、**kwds)

测试正则表达式是否与引发的异常的字符串表示形式匹配。regexp 可以是正则表达式对象或包含适合 re.search() 使用的正则表达式的字符串。

以下示例显示了如何使用assertRaisesRegexp() -

import unittest
import re

class raiseTest(unittest.TestCase):
   def testraiseRegex(self):
      self.assertRaisesRegexp(TypeError, "invalid", reg,"Point","TutorialsPoint")
      
if __name__ == '__main__':
   unittest.main()

在这里, testraseRegex() 测试作为第一个参数不会失败。在第二个参数字符串中找到“Point”。

================================================================
FAIL: testraiseRegex (__main__.raiseTest)
----------------------------------------------------------------------
Traceback (most recent call last):
   File "C:/Python27/raiseTest.py", line 11, in testraiseRegex
      self.assertRaisesRegexp(TypeError, "invalid", reg,"Point","TutorialsPoint")
AssertionError: TypeError not raised
----------------------------------------------------------------------

然而,变化如下所示 -

self.assertRaisesRegexp(TypeError, "invalid", reg,123,"TutorialsPoint")

将抛出 TypeError 异常。因此,将显示以下结果 -

================================================================
FAIL: testraiseRegex (__main__.raiseTest)
----------------------------------------------------------------------
Traceback (most recent call last):
   File "raisetest.py", line 11, in testraiseRegex
      self.assertRaisesRegexp(TypeError, "invalid", reg,123,"TutorialsPoint")
AssertionError: "invalid" does not match 
   "first argument must be string or compiled pattern"
----------------------------------------------------------------------

单元测试框架 - 时间测试

Junit,Java 单元测试框架(Pyunit 是 JUnit 的实现)有一个方便的超时选项。如果测试花费的时间超过指定时间,则会将其标记为失败。

Python 的测试框架不包含任何超时支持。然而,名为 timeout-decorator 的第三部分模块可以完成这项工作。

从以下位置下载并安装模块 -

https://pypi.python.org/packages/source/t/timeout-decorator/timeout-decorator-0.3.2.tar.gz

  • 在代码中导入timeout_decorator
  • 在测试之前放置超时装饰器
  • @timeout_decorator.timeout(10)

如果此行下方的测试方法花费的时间超过此处提到的超时(10 分钟),则会引发 TimeOutError。例如 -

import time
import timeout_decorator

class timeoutTest(unittest.TestCase):

   @timeout_decorator.timeout(5)
   def testtimeout(self):
      print "Start"
   for i in range(1,10):
      time.sleep(1)
      print "%d seconds have passed" % i
      
if __name__ == '__main__':
   unittest.main()

单元测试框架 - Unittest2

unittest2 是 Python 2.7 及更高版本中添加到 Python 测试框架的附加功能的向后移植。它经过测试可在 Python 2.6、2.7 和 3.* 上运行。最新版本可以从https://pypi.python.org/pypi/unittest2下载

要使用unittest2而不是unittest,只需将importunittest替换为importunittest2即可。

unittest2 中的类派生自unittest 中的相应类,因此应该可以使用unittest2 测试运行基础结构,而不必立即将所有测试切换为使用unittest2。如果您打算实现新功能,请从unittest2.TestCase而不是unittest.TestCase继承您的测试用例

以下是unittest2的新功能 -

  • addCleanups以实现更好的资源管理

  • 包含许多新的断言方法

  • assertRaises作为上下文管理器,随后可以访问异常

  • 具有模块级固定装置,例如setUpModuletearDownModule

  • 包括用于从模块或包加载测试的load_tests协议

  • TestResult 上的startTestRunstopTestRun方法

在 Python 2.7 中,您可以使用python -m unittest <args>调用 unittest 命令行功能(包括测试发现)。

相反,unittest2 带有一个脚本unit2。

unit2 discover
unit2 -v test_module

单元测试框架 - 信号处理

在测试运行期间更有效地处理 control-C 是由 unittest 的 -c/--catch 命令行选项以及catchbreak参数提供的。启用 catch 中断Behave后,control-C 将允许当前运行的测试完成,然后测试运行将结束并报告迄今为止的所有结果。第二个 control-c 将以通常的方式引发键盘中断。

如果调用了unittest处理程序但未安装signal.SIGINT处理程序,则它会调用默认处理程序。这通常是替换已安装处理程序并委托给它的代码的预期Behave。对于需要禁用unittest control-c 处理的单个测试,可以使用removeHandler() 装饰器。

以下实用程序函数在测试框架内启用 control-c 处理功能 -

单元测试.installHandler()

安装 control-c 处理程序。当收到signal.SIGINT时,所有注册结果都会被调用 TestResult.stop()。

单元测试.registerResult(结果)

注册一个TestResult对象以进行 control-c 处理。注册结果会存储对其的弱引用,因此不会阻止结果被垃圾收集。

单元测试.removeResult(结果)

删除已注册的结果。一旦结果被删除,则将不再在该结果对象上调用 TestResult.stop() 以响应 control-c。

unittest.removeHandler(函数=无)

当不带参数调用时,此函数将删除 control-c 处理程序(如果已安装)。此函数还可以用作测试装饰器,以在执行测试时临时删除处理程序。

图形用户界面测试运行器

安装unittest模块是为了以交互方式发现和运行测试。该实用程序是一个 Python 脚本“inittestgui.py”,它使用 Tkinter 模块,该模块是 TK 图形工具包的 Python 端口。它提供了一个易于使用的 GUI,用于发现和运行测试。

Python unittestgui.py
运行测试

单击“发现测试”按钮。将出现一个小对话框,您可以在其中选择要运行测试的目录和模块。

发现测试

最后,单击开始按钮。将从选定的路径和模块名称中发现测试,结果窗格将显示结果。

结果窗格

为了查看单个测试的详细信息,请选择并单击结果框中的测试 -

个别测试详情

如果您在 Python 安装中没有找到此实用程序,您可以从项目页面http://pyunit.sourceforge.net/获取它。

类似地,基于 wxpython 工具包的实用程序也可用。

单元测试框架 - Doctest

Python 标准发行版包含“Doctest”模块。该模块的功能使得搜索看起来像交互式 Python 会话的文本片段成为可能,并执行这些会话以查看它们是否完全按照所示方式工作。

Doctest 在以下场景中非常有用 -

  • 通过验证所有交互式示例是否仍然按记录工作来检查模块的文档字符串是否是最新的。

  • 通过验证测试文件或测试对象中的交互式示例是否按预期工作来执行回归测试。

  • 为包编写教程文档,用输入输出示例进行大量说明

在 Python 中,“docstring”是一个字符串文字,它作为类、函数或模块中的第一个表达式出现。执行套件时它会被忽略,但编译器会识别它并放入封闭类、函数或模块的__doc__属性中。由于它可以通过内省获得,因此它是对象文档的规范位置。

通常的做法是将 Python 代码的不同部分的示例用法放在文档字符串中。doctest 模块允许验证这些文档字符串是否与代码中的间歇性修订保持同步。

在以下代码中,定义了阶乘函数并穿插了示例用法。为了验证示例用法是否正确,请调用 doctest 模块中的 testmod() 函数。

"""
This is the "example" module.

The example module supplies one function, factorial(). For example,

>>> factorial(5)
120
"""

def factorial(x):
   """Return the factorial of n, an exact integer >= 0.
   >>> factorial(-1)
   Traceback (most recent call last):
      ...
   ValueError: x must be >= 0
   """
   
   if not x >= 0:
      raise ValueError("x must be >= 0")
   f = 1
   for i in range(1,x+1):
      f = f*i
   return f
   
if __name__ == "__main__":
   import doctest
   doctest.testmod()

输入上述脚本并将其保存为 FactDocTest.py 并尝试从命令行执行该脚本。

Python FactDocTest.py

除非示例失败,否则不会显示任何输出。现在,将命令行更改为以下内容 -

Python FactDocTest.py –v

控制台现在将显示以下输出 -

C:\Python27>python FactDocTest.py -v
Trying:
   factorial(5)
Expecting:
   120
ok
Trying:
   factorial(-1)
Expecting:
   Traceback (most recent call last):
      ...
   ValueError: x must be >= 0
ok
2 items passed all tests:
   1 tests in __main__
   1 tests in __main__.factorial
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

另一方面,如果 Factorial() 函数的代码未在文档字符串中给出预期结果,则会显示失败结果。例如,在上面的脚本中将 f = 1 更改为 f = 2,然后再次运行 doctest。结果如下 -

Trying:
   factorial(5)
Expecting:
   120
**********************************************************************
File "docfacttest.py", line 6, in __main__
Failed example:
factorial(5)
Expected:
   120
Got:
   240
Trying:
   factorial(-1)
Expecting:
   Traceback (most recent call last):
      ...
   ValueError: x must be >= 0
ok
1 items passed all tests:
   1 tests in __main__.factorial
**********************************************************************
1 items had failures:
   1 of 1 in __main__
2 tests in 2 items.
1 passed and 1 failed.
***Test Failed*** 1 failures.

Doctest:检查文本文件中的示例

doctest 的另一个简单应用是测试文本文件中的交互式示例。这可以通过 testfile() 函数来完成。

以下文本存储在名为“example.txt”的文本文件中。

Using ''factorial''
-------------------
This is an example text file in reStructuredText format. First import
''factorial'' from the ''example'' module:
   >>> from example import factorial
Now use it:
   >>> factorial(5)
   120

文件内容被视为文档字符串。为了验证文本文件中的示例,请使用 doctest 模块的 testfile() 函数。

def factorial(x):
   if not x >= 0:
      raise ValueError("x must be >= 0")
   f = 1
   for i in range(1,x+1):
      f = f*i
   return f
   
if __name__ == "__main__":
   import doctest
   doctest.testfile("example.txt")
  • 与 testmod() 一样,testfile() 不会显示任何内容,除非示例失败。如果示例确实失败,则失败的示例和失败的原因将使用与 testmod() 相同的格式打印到控制台。

  • 在大多数情况下,交互式控制台会话的复制和粘贴工作正常,但 doctest 并不尝试对任何特定的 Python shell 进行精确模拟。

  • 任何预期输出必须立即跟在包含代码的最后“>>>”或“...”行之后,并且预期输出(如果有)扩展到下一个“>>>”或全空白行。

  • 预期输出不能包含全空白行,因为这样的行用于表示预期输出的结束。如果预期的输出确实包含空行,请将 <BLANKLINE> 放入您的 doctest 示例中,每个地方都需要一个空行。

单元测试框架 - Doctest API

doctest API 围绕以下两个容器类,用于存储文档字符串中的交互式示例 -

  • 示例- 单个 Python 语句与其预期输出配对。

  • DocTest - 示例的集合,通常从单个文档字符串或文本文件中提取。

定义以下附加处理类来查找、解析、运行和检查 doctest 示例 -

  • DocTestFinder - 查找给定模块中的所有文档字符串,并使用 DocTestParser 从包含交互式示例的每个文档字符串创建文档测试。

  • DocTestParser - 从字符串(例如对象的文档字符串)创建文档测试对象。

  • DocTestRunner - 执行 doctest 中的示例,并使用 OutputChecker 来验证其输出。

  • OutputChecker - 将 doctest 示例的实际输出与预期输出进行比较,并确定它们是否匹配。

DocTestFinder 类

它是一个处理类,用于从给定对象的文档字符串及其所包含对象的文档字符串中提取与给定对象相关的文档测试。目前可以从以下对象类型中提取文档测试:模块、函数、类、方法、静态方法、类方法和属性。

此类定义了 find() 方法。它返回由对象的文档字符串或其包含的任何对象的文档字符串定义的 DocTests 列表。

DocTestParser 类

它是一个处理类,用于从字符串中提取交互式示例,并使用它们创建 DocTest 对象。此类定义了以下方法 -

  • get_doctest() - 从给定字符串中提取所有 doctest 示例,并将它们收集到DocTest对象中。

  • get_examples(string[, name]) - 从给定字符串中提取所有 doctest 示例,并将它们作为示例对象列表返回。行号从 0 开始。可选参数名称是标识该字符串的名称,仅用于错误消息。

  • parse(string[, name]) - 将给定字符串划分为示例和中间文本,并将它们作为交替示例和字符串的列表返回。示例的行号是从 0 开始的。可选参数名称是标识该字符串的名称,仅用于错误消息。

DocTestRunner 类

这是一个处理类,用于执行和验证 DocTest 中的交互式示例。其中定义了以下方法 -

报告开始()

报告测试运行程序即将处理给定的示例。提供此方法是为了允许DocTestRunner的子类自定义其输出;不应该直接调用它

报告_成功()

报告给定的示例运行成功。提供此方法是为了允许 DocTestRunner 的子类自定义其输出;不应该直接调用它。

报告失败()

报告给定的示例失败。提供此方法是为了允许DocTestRunner的子类自定义其输出;不应该直接调用它。

报告意外异常()

报告给定的示例引发了意外异常。提供此方法是为了允许 DocTestRunner 的子类自定义其输出;不应该直接调用它。

运行(测试)

运行test中的示例(DocTest 对象),并使用 writer 函数out显示结果。

总结([详细])

打印此 DocTestRunner 已运行的所有测试用例的摘要,并返回命名元组TestResults(failed, attempts)。可选的详细参数控制摘要的详细程度。如果未指定详细程度,则使用 DocTestRunner 的详细程度。

输出检查器类

此类用于检查 doctest 示例的实际输出是否与预期输出匹配。

此类中定义了以下方法 -

检查输出()

如果示例的实际输出 ( got ) 与预期输出 ( want )匹配,则返回True 。如果这些字符串相同,则始终认为它们匹配;但根据测试运行程序使用的选项标志,也可能有几种非精确匹配类型。有关选项标志的更多信息,请参阅选项标志指令部分。

输出差异()

返回一个字符串,描述给定示例的预期输出 ( example ) 和实际输出 ( got )之间的差异。

DocTest 与单元测试集成

doctest 模块提供了两个函数,可用于从包含 doctest 的模块和文本文件创建 unittest 测试套件。要与 unittest 测试发现集成,请在测试模块中包含 load_tests() 函数 -

import unittest
import doctest
import doctestexample

def load_tests(loader, tests, ignore):
   tests.addTests(doctest.DocTestSuite(doctestexample))
   return tests

将形成来自 unittest 和 doctest 的测试的组合 TestSuite,现在可以通过 unittest 模块的 main() 方法或 run() 方法执行它。

以下是使用 doctests 从文本文件和模块创建unittest.TestSuite实例的两个主要函数 -

doctest.DocFileSuite()

它用于将 doctest 测试从一个或多个文本文件转换为unittest.TestSuite。返回的unittest.TestSuite将由unittest框架运行并运行每个文件中的交互式示例。如果文件中的任何示例失败,则合成的单元测试将失败,并引发failureException异常,显示包含测试的文件的名称和(有时是近似的)行号。

doctest.DocTestSuite()

它用于将模块的 doctest 测试转换为unittest.TestSuite

返回的unittest.TestSuite将由unittest框架运行并运行模块中的每个doctest。如果任何文档测试失败,则合成的单元测试失败,并引发failureException异常,显示包含测试的文件的名称和(有时是近似的)行号

在幕后,DocTestSuite()从 doctest.DocTestCase 实例中创建了一个unittest.TestSuite ,而 DocTestCase 是 unittest.TestCase 的子类。

类似地,DocFileSuite()创建一个unittest.Tes