原文地址:http://www.tuicool.com/articles/263m22
背景: 大型软件系统的开发是一个很复杂的过程,其中因为人的因素而所产生的错误非常多,因此软件在开发过程必须要有相应的质量保证活动,而软件测试则是保证质量的关键措施。正像软件熵(software entropy)所描述的那样:一个程序从设计很好的状态开始,随着新的功能不断地加入,程序逐渐地失去了原有的结构,最终变成了一团乱麻(其实最初的”很好的状态”得加个问号)。测试的目的说起来其实很简单也极具吸引力,那就是写出高质量的软件并解决软件熵这一问题。
使用Python语言的开发人员可以使用Steve Purcell编写的PyUnit作为单元测试框架,通过将单元测试融合到PyUnit这一测试框架里,Python程序员可以更容易地增加、管理、执行测试用例,并对测试结果进行分析。此外,使用PyUnit还可以实现自动单元测试(回归测试)
规范Python单元测试
测试是一个贯穿于整个开发过程的连续过程,从某个意义上说,软件开发的过程实际上就是测试过程。正如Martin Fowler所说的”在你不知道如何测试代码之前,就不该编写程序。而一旦你完成了程序,测试代码也应该完成。除非测试成功,你不能认为你编写出了可以工作的程序。”
手工测试
手工测试方法存在许多问题。首先,测试程序的写法没有一定的规范可以遵循,十个程序员完全可能写出十种不同的测试程序来,如果每个Python程序员都有自己不同的设计测试类的方法,光维护被测试的类就够麻烦了,谁还顾得上维护测试类。其次,需要编写大量的辅助代码才能进行单元测试
单元测试
让单元测试代码能够被测试和维护人员更容易地理解,最好的解决办法是让开发人员遵循一定的规范来编写用于测试的代码,具体到Python程序员来讲,则是要采用PyUnit这一自动测试框架来构造单元测试用例。目前PyUnit已经得到了大多数Python开发人员的认可,成了事实上的单元测试标准。
实例
编写一个待测试的SparkContext类:
from pyspark import SparkConf,SparkContext
class SparkContext:
def __init__(self):
self.__conf = SparkContext().setMaster("local").setAppName("movie")
self.__sc = SparkContext(conf=self.__conf)
def get_sc(self):
'''
:return: SparkContext
'''
return self.__sc
def get_conf(self):
'''
:return: SparkConf
'''
return self.__conf
采用Pyunit进行测试:
from kesaiRecommend.src.sc import SparkContext
import unittest
class SparkContextTest(unittest.TestCase):
def setUp(self):
self.sparkContext = SparkContext()
def tearDown(self):
self.sparkContext = None
def testGetsc(self):
self.assertIsNotNone(self.sparkContext.get_sc())
def testGetconf(self):
self.assertIsNotNone(self.sparkContext.get_conf())
def suite():
'''
:return: TestSuite
'''
suite = unittest.TestSuite()
suite.addTest(SparkContextTest('testGetsc'))
suite.addTest(SparkContextTest('testGetconf'))
if __name__ == "__main__":
unittest.main(defaultTest='suite')
代码说明:
1. 用import语句引入unittest模块(python自带包)
2. 让所有执行测试的类都继承于TestCase类,可以将TestCase看成是对特定类进行测试的方法的集合。
3. 在setUp()方法中进行测试前的初始化工作,并在tearDown()方法中执行测试后的清除工作,setUp()和tearDown()都是TestCase类中定义的方法。
4. 在testGetsc中调用assertIsNotNone方法,查看能否赋值成功
5. 提供名为suite()的全局方法,PyUnit在执行测试的过程调用suit()方法来确定有多少个测试用例需要被执行,可以将TestSuite看成是包含所有测试用例的一个容器。
6. 每个测试用例,都会自己执行一遍setUp()和tearDown()
PyUnit使得所有的Python程序员都可以使用同样的单元测试方法,测试过程不再是杂乱无章的了,而是在同一规范指导下进行的有序行为,这就是使用PyUnit这一自动单元测试框架所带来的最大好处。
自动测试框架PyUnit
在对软件测试理论和PyUnit有了一个大致了解之后,下面辅以具体的实例介绍Python程序员如何借助PyUnit来进行单元测试。在Python中进行单元测试时需要用到PyUnit模块,Python 2.1及其以后的版本都将PyUnit作为一个标准模块,可以导入相应模块,使用即可
测试用例TestCase
软件测试中最基本的组成单元是 测试用例 (test case),PyUnit使用TestCase类来表示测试用例,并要求所有用于执行测试的类都必须从该类继承。TestCase子类实现的测试代码应该是自包含(self contained)的,也就是说测试用例既可以单独运行,也可以和其它测试用例构成集合共同运行。
TestCase在PyUnit测试框架中被视为测试单元的运行实体,Python程序员可以通过它派生自定义的测试过程与方法(测试单元),利用Command和Composite设计模式,多个TestCase还可以组合成测试用例集合。PyUnit测试框架在运行一个测试用例时,TestCase子类定义的setUp()、runTest()和tearDown()方法被依次执行,最简单的测试用例只需覆盖runTest()方法来执行特定的测试代码就可以了
import unittest
# 执行测试的类
class WidgetTestCase(unittest.TestCase):
def runTest(self):
widget = Widget()
self.assertEqual(widget.getSize(), (40, 40))
PyUnit测试框架中构造上述WidgetTestCase类的一个实例,应该不带任何参数调用其构造函数:
testCase = WidgetTestCase()
一个测试用例通常只对软件模块中的一个方法进行测试,采用覆盖runTest()方法来构造测试用例在PyUnit中称为 静态方法 ,如果要对同一个软件模块中的多个方法进行测试,通常需要构造多个执行测试的类
import unittest
# 测试getSize()方法的测试用例
class WidgetSizeTestCase(unittest.TestCase):
def runTest(self):
widget = Widget()
self.assertEqual(widget.getSize(), (40, 40))
# 测试resize()方法的测试用例
class WidgetResizeTestCase(unittest.TestCase):
def runTest(self):
widget = Widget()
widget.resize(100, 100)
self.assertEqual(widget.getSize(), (100, 100))
问题: 采用静态方法,Python程序员不得不为每个要测试的方法编写一个测试类(该类通过覆盖runTest()方法来执行测试),并在每一个测试类中生成一个待测试的对象
动态:采用PyUnit提供的 动态方法 ,只编写一个测试类来完成对整个软件模块的测试,这样对象的初始化工作可以在setUp()方法中完成,而资源的释放则可以在tearDown()方法中完成
import unittest
# 执行测试的类
class WidgetTestCase(unittest.TestCase):
def setUp(self):
self.widget = Widget()
def tearDown(self):
self.widget.dispose()
self.widget = None
def testSize(self):
self.assertEqual(self.widget.getSize(), (40, 40))
def testResize(self):
self.widget.resize(100, 100)
self.assertEqual(self.widget.getSize(), (100, 100))
采用动态方法最大的好处是测试类的结构非常好,用于测试一个软件模块的所有代码都可以在同一个类中实现。动态方法不再覆盖runTest()方法,而是为测试类编写多个测试方法( 按习惯这些方法通常以test开头 ),在创建TestCase子类的实例时必须给出测试方法的名称,来为PyUnit测试框架指明运行该测试用例时究竟应该调用测试类中的哪个方法
sizeTestCase = WidgetTestCase(“testSize”)
resizeTestCase = WidgetTestCase(“testResize”)
测试用例集TestSuite
完整的单元测试很少只执行一个测试用例,开发人员通常都需要编写多个测试用例才能对某一软件功能进行比较完整的测试,这些相关的测试用例称为一个测试用例集,在PyUnit中是用TestSuite类来表示的
在创建了一些TestCase子类的实例作为测试用例之后,下一步要做的工作就是用TestSuit类来组织它们。PyUnit测试框架允许Python程序员在单元测试代码中定义一个名为suite()的全局函数,并将其作为整个单元测试的入口,PyUnit通过调用它来完成整个测试过程
def suite():
suite = unittest.TestSuite()
suite.addTest(WidgetTestCase("testSize"))
suite.addTest(WidgetTestCase("testResize"))
return suite
可以直接定义一个TestSuite的子类,并在其初始化方法(init)中完成所有测试用例的添加:
class WidgetTestSuite(unittest.TestSuite):
def __init__(self):
unittest.TestSuite.__init__(self, map(WidgetTestCase,
("testSize",
"testResize")))
只需要在suite()方法中返回该类的一个实例就可以了:
def suite():
return WidgetTestSuite()
更简洁方法:如果用于测试的类中所有的测试方法都以test开,Python程序员甚至可以用PyUnit模块提供的makeSuite()方法来构造一个TestSuite
def suite():
return unittest.makeSuite(WidgetTestCase, “test”)
在PyUnit测试框架中,TestSuite类可以看成是TestCase类的一个容器,用来对多个测试用例进行组织,这样多个测试用例可以自动在一次测试中全部完成。事实上,TestSuite除了可以包含TestCase外,也可以包含TestSuite,从而可以构成一个更加庞大的测试用例集
suite1 = mysuite1.TheTestSuite()
suite2 = mysuite2.TheTestSuite()
alltests = unittest.TestSuite((suite1, suite2))
* 实施测试*
编写测试用例(TestCase)并将它们组织成测试用例集(TestSuite)的最终目的只有一个:实施测试并获得最终结果。PyUnit使用TestRunner类作为测试用例的基本执行环境,来驱动整个单元测试过程。Python开发人员在进行单元测试时一般不直接使用TestRunner类,而是使用其子类TextTestRunner来完成测试,并将测试结果以文本方式显示出来
runner = unittest.TextTestRunner()
runner.run(suite)
TestRunner来实施测试的例子:
from widget import Widget
import unittest
# 执行测试的类
class WidgetTestCase(unittest.TestCase):
def setUp(self):
self.widget = Widget()
def tearDown(self):
self.widget.dispose()
self.widget = None
def testSize(self):
self.assertEqual(self.widget.getSize(), (40, 40))
def testResize(self):
self.widget.resize(100, 100)
self.assertEqual(self.widget.getSize(), (100, 100))
# 测试
if __name__ == "__main__":
# 构造测试集
suite = unittest.TestSuite()
suite.addTest(WidgetTestCase("testSize"))
suite.addTest(WidgetTestCase("testResize"))
# 执行测试
runner = unittest.TextTestRunner()
runner.run(suite)
说明:默认情况下,TextTestRunner将结果输出到sys.stderr上,但如果在创建TextTestRunner类实例时将一个文件对象传递给了构造函数,则输出结果将被重定向到该文件中。在Python的交互环境中驱动单元测试时,使用TextTestRunner类是一个不错的选择
PyUnit模块中定义了一个名为main的全局方法,使用它可以很方便地将一个单元测试模块变成可以直接运行的测试脚本,main()方法使用TestLoader类来搜索所有包含在该模块中的测试方法,并自动执行它们。如果Python程序员能够按照约定(以test开头)来命名所有的测试方法,那就只需要在测试模块的最后加入如下几行代码即可
from widget import Widget
import unittest
# 执行测试的类
class WidgetTestCase(unittest.TestCase):
def setUp(self):
self.widget = Widget()
def tearDown(self):
self.widget.dispose()
self.widget = None
def testSize(self):
self.assertEqual(self.widget.getSize(), (40, 40))
def testResize(self):
self.widget.resize(100, 100)
self.assertEqual(self.widget.getSize(), (100, 100))
# 测试
if __name__ == "__main__":
unittest.main()
为了使单元测试更具亲合力,PyUnit软件包中还提供了一个图形界面测试脚本unittestgui.py,将其复制到当前目录后,可以执行下面的命令来启动该测试工具,对main_runner.py脚本中的所有测试用例进行测试
python unittestgui.py main_runner
忽略测试案例( Python2.7支持)
可以分无条件忽略和有条件忽略,通过装饰器实现
class MyTestCase(unittest.TestCase):
@unittest.skip("demonstrating skipping")
def test_nothing(self):
self.fail("shouldn't happen")
@unittest.skipIf(mylib.__version__ < (1, 3),
"not supported in this library version")
def test_format(self):
# Tests that work for only a certain version of the library.
pass
@unittest.skipUnless(sys.platform.startswith("win"), "requires Windows")
def test_windows_support(self):
# windows specific testing code
pass
测试类也可以忽略
@unittest.skip("showing class skipping")
class MySkippedTestCase(unittest.TestCase):
def test_not_run(self):
pass
总结:
参考博客: 深入理解unittest
- 一个TestCase的实例就是一个测试用例。什么是测试用例呢?就是一个完整的测试流程,包括测试前准备环境的搭建(setUp),执行测试代码(run),以及测试后环境的还原(tearDown)。元测试(unit test)的本质也就在这里,一个测试用例是一个完整的测试单元,通过运行这个测试单元,可以对某一个问题进行验证。
- 而多个测试用例集合在一起,就是TestSuite,而且TestSuite也可以嵌套TestSuite。
- TestLoader是用来加载TestCase到TestSuite中的,其中有几个loadTestsFrom__()方法,就是从各个地方寻找TestCase,创建它们的实例,然后add到TestSuite中,再返回一个TestSuite实例。
- TextTestRunner是来执行测试用例的,其中的run(test)会执行TestSuite/TestCase中的run(result)方法。
- 测试的结果会保存到TextTestResult实例中,包括运行了多少测试用例,成功了多少,失败了多少等信息。
unittest断言
自动化脚本最重要的是断言,正确设置断言以后才能帮助我们判断测试用例执行结果。列举一下常用断言:
assertEqual(a, b) a == b
assertNotEqual(a, b) a != b
assertTrue(x) bool(x) is True
assertFalse(x) bool(x) is False
assertIs(a, b) a is b 2.7
assertIsNot(a, b) a is not b 2.7
assertIsNone(x) x is None 2.7
assertIsNotNone(x) x is not None 2.7
assertIn(a, b) a in b 2.7
assertNotIn(a, b) a not in b 2.7
assertIsInstance(a, b) isinstance(a, b) 2.7
assertNotIsInstance(a, b) not isinstance(a, b) 2.7
assertAlmostEqual(a, b) round(a-b, 7) == 0
assertNotAlmostEqual(a, b) round(a-b, 7) != 0
assertGreater(a, b) a > b 2.7
assertGreaterEqual(a, b) a >= b 2.7
assertLess(a, b) a < b 2.7
assertLessEqual(a, b) a <= b 2.7
assertRegexpMatches(s, re) regex.search(s) 2.7
assertNotRegexpMatches(s, re) not regex.search(s) 2.7
assertItemsEqual(a, b) sorted(a) == sorted(b) and works with unhashable objs 2.7
assertDictContainsSubset(a, b) all the key/value pairs in a exist in b 2.7
assertMultiLineEqual(a, b) strings 2.7
assertSequenceEqual(a, b) sequences 2.7
assertListEqual(a, b) lists 2.7
assertTupleEqual(a, b) tuples 2.7
assertSetEqual(a, b) sets or frozensets 2.7
assertDictEqual(a, b) dicts 2.7
assertMultiLineEqual(a, b) strings 2.7
assertSequenceEqual(a, b) sequences 2.7
assertListEqual(a, b) lists 2.7
assertTupleEqual(a, b) tuples 2.7
assertSetEqual(a, b) sets or frozensets 2.7
assertDictEqual(a, b) dicts 2.7
小结
测试是保证软件质量的关键,新的软件开发方法要求程序员在编写代码前先编写测试用例,并在软件开发过程中不断地进行单元测试,从而最大限度地减少缺陷(Bug)的产生。软件单元测试是XP方法的基石,测试框架为程序员进行单元测试提供了统一的规范,Python程序员可以使用PyUnit作为软件开发过程中的自动单元测试框架。