# 什么是单元测试? 单元测试(又称为模块测试, Unit Testing)是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。单元测试粒度最小,一般由开发人员采用白盒方式来测试,主要测试单元是否符合设计。单元测试的主要过程仍是通过给定的输入,判断得到的结果是否符合预期的代码结果测试的过程。 # 为什么需要单元测试? 单元测试有以下好处: 1.确保代码质量。 2.改善代码设计,难以测试的代码一般是设计不够简洁的代码。 3.保证重构不会引入新问题,以函数为单位进行重构的时候,只需要重新跑测试就基本可以保证重构没引入新问题。 4.通过单元测试,可以增强代码的执行与预期一致,增强对于代码的自信。 5.在测试驱动编程的理念中,首先程序员要编写测试程序,然后编写可以通过测试的程序。测试程序就是程序的需求说明,它能够帮助程序员在开发程序时,不偏离需求。TTD[Test-Driven Development]最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。 # 怎么编写单元测试? 对于Python代码而言,常用的测试工具有doctest和unittest。doctest是简单一些的模块,是检测文档用的。doctet.test_mod函数从一个模块中读取所有文档字符串,找出所有看起来像是在交互式解释器中输入的例子的文本,之后检查例子是否符合实际要求。在实际工作中,为python写单元测试时更加强大和常用的模块是unittest模块,unittest基于Java的流行测试框架Junit,通过使用unittest我们可以以结构化的方式编写大型而且周详的测试集。 # unittest模块 unittest 模块使用的模式有三种,如下: ## 通过unittest.main()来执行测试用例的方式: import unittest class UCTestCase(unittest.TestCase): def setUp(self): #测试前需执行的操作 ..... def tearDown(self): #测试用例执行完后所需执行的操作 ..... # 测试用例1 def testCreateFolder(self): #具体的测试脚本 ...... # 测试用例2 def testDeleteFolder(self): #具体的测试脚本 ...... if __name__ == "__main__": unittest.main() ## 通过testsuit来执行测试用例的方式: import unittest # 执行测试的类 class UCTestCase(unittest.TestCase): def setUp(self): #测试前需执行的操作 ..... def tearDown(self): #测试用例执行完后所需执行的操作 ..... # 测试用例1 def testCreateFolder(self): #具体的测试脚本 ...... def testDeleteFolder(self): # 具体的测试脚本 If __name__ == "__main__": # 构造测试集, 添加测试用例 suite = unittest.TestSuite() suite.addTest(UC7TestCase("testCreateFolder")) suite.addTest(UC7TestCase("testDeleteFolder")) #执行测试, 构造runner。 runner = unittest.TextTestRunner() runner.run(suite) ## 通过testloader运行测试集: import unittest class TestCase1(unittest.TestCase): #def setUp(self): #def tearDown(self): def testCase1(self): print 'aaa' def testCase2(self): print 'bbb' class TestCase2(unittest.TestCase): #def setUp(self): #def tearDown(self): def testCase1(self): print 'aaa1' def testCase2(self): print 'bbb1' if __name__ == "__main__": #此用法可以同时测试多个类 suite1=unittest.TestLoader().loadTestsFromTestCase(TestCase1) suite2=unittest.TestLoader().loadTestsFromTestCase(TestCase2) suite = unittest.TestSuite([suite1, suite2]) unittest.TextTestRunner(verbosity=2).run(suite) # Mock和MagicMock 在单元测试进行的同时,就离不开mock模块的存在,初次接触这个概念的时候会有这样的疑问:把要测的东西都模拟掉了还测试什么呢? 但在实际生产中的项目是非常复杂的,对其进行单元测试的时候,会遇到以下问题: •接口的依赖 •外部接口调用 •测试环境非常复杂 单元测试应该只针对当前单元进行测试, 所有的内部或外部的依赖应该是稳定的, 已经在别处进行测试过的。使用mock 就可以对外部依赖组件实现进行模拟并且替换掉, 从而使得单元测试将焦点只放在当前的单元功能。因为在为代码进行单元测试的同时,会发现该模块依赖于其他的模块,例如数据库,网络,或者第三方模块的存在,而我们对一个模块进行单元测试的目的,是测试当前模块正常工作,这样就要避开对其他模块的依赖,而mock主要作用便在于,专注于待测试的代码。而在单元测试中,如何灵活的使用mock模块是核心所在。 ## mock模块的使用 在mock模块中,两个常用的类型为Mock,MagicMock,两个类的关系是MagicMock继承自Mock,最重要的两个属性是return_value, side_effect。 >>> from mock import Mock >>> fake_obj = Mock() >>>fake_obj.return_value = 'This is a mock object' >>> fake_obj() 'This is a mock object' 我们通过Mock()可以创建一个mock对象,通过renturn_value 指定它的返回值。即当下文出现fake_obj()会返回其return_value所指定的值。 也可以通过side_effect指定它的副作用,这个副作用就是当你调用这个mock对象是会调用的函数,也可以选择抛出一个异常,来对程序的错误状态进行测试。 >>>def b(): ... print 'This is b' ... >>>fake_obj.side_effect = b >>>fake_obj() This is b >>>fake_obj.side_effect = KeyError('This is b') >>>fake_obj() ... KeyError: 'This is b' 如果要模拟一个对象而不是函数,你可以直接在mock对象上添加属性和方法,并且每一个添加的属性都是一个mock对象,也就是说可以对这些属性进行配置,并且可以一直递归的定义下去。 >>>fake_obj.fake_a.return_value = 'This is fake_obj.fake_a' >>>fake_obj.fake_a() 'This is fake_obj.fake_a' 上述代码片段中fake_obj是一个mock对象,而fake_obj.fake_a的这种形式使得fake_a变成了fake_obj的一个属性,作用是在fake_obj.fake_a()调用时会返回其return_value。 另外也可以通过为side_effect指定一个列表,这样在每次调用时会依次返回,如下: >>> fake_obj = Mock(side_effect = [1, 2, 3]) >>>fake_obj() 1 >>>fake_obj() 2 >>>fake_obj() 3 ## 函数如何mock 在rbd_api.py文件中如下内容: import DAO_PoolMgr def checkpoolstat(pool_name) ret, poolstat = DAO_PoolMgr.DAO_query_ispoolok(pool_name) if ret != MGR_COMMON.MONGO_SUCCESS: return ret if poolstat is False: return MGR_COMMON.POOL_STAT_ERROR return MGR_COMMON.SUCCESS 要为这个函数撰写单元测试,因为其有数据库的操作,因而就需要mock 出DAO_query_ispoolok操作。 因此,我们在test_rbd_api.py文件中可以这么写:因为DAO_query_ispoolok是类DAO_PoolMgr的操作,因此可以这么写: #!/usr/bin/python import DAO_PoolMgr import unittest import rbd_api as rbdAPI class TestAuxiliaryFunction(unittest.TestCase): def setUp(self): self.pool_name = "aaa" def tearDown(self): self.pool_name = None @mock.patch.object(DAO_PoolMgr, "DAO_query_ispoolok") def test_checkpoolstat(self, mock_DAO_query_ispoolok): mock_DAO_query_ispoolok.return_value = (MGR_COMMON.POOL_STAT_ERROR, None) self.assert(rbdAPI.checkpoolstat(self.pool_name), MGR_COMMON.POOL_STAT_ERROR) mock_DAO_query_ispoolok.return_value = (MGR_COMMON.SUCCESS, False) self.assert(rbdAPI.checkpoolstat(self.pool_name), MGR_COMMON.POOL_STAT_ERROR) mock_DAO_query_ispoolok.return_value = (MGR_COMMON.SUCCESS, True) self.assert(rbdAPI.checkpoolstat(self.pool_name), MGR_COMMON.SUCCESS) 测试用例上的装饰器含义如下: @mock.pathc.object(类名,“类中函数名”),而如果想要忽略某个测试用例,则可以通过装饰器@unittest.skip(“原因”) 而对于另外一种情形则是在另外一个函数中调用了checkpoolstat函数。 如下rbd_api.py: def checkpoolstat(): …… class Disk(Resource): def __init__(self): …… def delete(self, pool, img): ret = rbd_api.checkpoolstat() …… 这样,我们在为delete函数撰写单元测试时,也可以在test_rbd_api.py中使用如下的方式: import rbd_api class TestDisk(unittest.TestCase): def setup(): … def teardown(): … @mock.patch(“rbd_api.checkpoolstat”, Mock(return_value = True)) def test_delete(): # rbd_api.checkpoolstat 已经成为一个mock对象了,调用时返回True … 此时的装饰器应该为: @mock.patch(“模块名.函数名”)