You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

9.2 KiB

什么是单元测试?

单元测试(又称为模块测试, 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模块中两个常用的类型为MockMagicMock两个类的关系是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(“模块名.函数名”)