diff --git a/unittest.md b/unittest.md index da08012..6aa57a0 100644 --- a/unittest.md +++ b/unittest.md @@ -10,4 +10,185 @@ 5.在测试驱动编程的理念中,首先程序员要编写测试程序,然后编写可以通过测试的程序。测试程序就是程序的需求说明,它能够帮助程序员在开发程序时,不偏离需求。TTD[Test-Driven Development]最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。 # 怎么编写单元测试? -对于Python代码而言,常用的测试工具有doctest和unittest。doctest是简单一些的模块,是检测文档用的。doctet.test_mod函数从一个模块中读取所有文档字符串,找出所有看起来像是在交互式解释器中输入的例子的文本,之后检查例子是否符合实际要求。在实际工作中,为python写单元测试时更加强大和常用的模块是unittest模块,unittest基于Java的流行测试框架Junit,通过使用unittest我们可以以结构化的方式编写大型而且周详的测试集。 \ No newline at end of file +对于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(“模块名.函数名”)