单元测试(又称为模块测试, Unit Testing)是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。单元测试粒度最小,一般由开发人员采用白盒方式来测试,主要测试单元是否符合设计。单元测试的主要过程仍是通过给定的输入,判断得到的结果是否符合预期的代码结果测试的过程。
总的来说,单元测试有以下好处:
对于Python 代码而言,常用的测试工具有doctest和unittest,doctest是简单一些的模块,是检测文档用的。doctet.test_mod函数从一个模块中读取所有文档字符串,找出所有看起来像是在交互式解释器中输入的例子的文本,之后检查例子是否符合实际要求。
定义如下函数代码square,求一个数的平凡,并且在它的文档字符串中添加两个例子,文件名为my_math.py
#!/usr/bin/python
import doctest
def square(x):
'''
Squares a number and returns the result
>>> square(2)
4
>>> square(3)
9
'''
return x*x
if __name__ == '__main__':
import my_math
doctest.testmod(my_math)
上述my_math.py文件,是较为标准的文档字符串格式,诸如>>>后有一个空格,’’’后的英文文档后有一个空行。
之后我们可以在Linux 命令行中运行查看测试结果。
[root@centos ~]# python my_math.py
[root@centos ~]# python my_math.py -v
Trying:
square(2)
Expecting:
4
ok
Trying:
square(3)
Expecting:
9
ok
1 items had no tests:
my_math
1 items passed all tests:
2 tests in my_math.square
2 tests in 2 items.
2 passed and 0 failed.
Test passed.
在实际工作中,为python写单元测试时更加强大和常用的模块是unittest模块,unittest基于Java的流行测试框架Junit,通过使用unittest我们可以以结构化的方式编写大型而且周详的测试集。Unittest文件位置在/usr/lib64/python2.7/unittest/init.pyc。unittest框架的主要组成部分
dir(unittest)
[‘BaseTestSuite’, ‘FunctionTestCase’, ‘SkipTest’, ‘TestCase’, ‘TestLoader’, ‘TestProgram’, ‘TestResult’, ‘TestSuite’, ‘TextTestResult’, ‘TextTestRunner’, ‘_TextTestResult’, ‘all’, ‘builtins’, ‘doc’, ‘file’, ‘name’, ‘package’, ‘path’, ‘__unittest’, ‘_expectedFailureInRpmBuild’, ‘_skipInRpmBuild’, ‘case’, ‘defaultTestLoader’, ‘expectedFailure’, ‘findTestCases’, ‘getTestCaseNames’, ‘installHandler’, ‘loader’, ‘main’, ‘makeSuite’, ‘registerResult’, ‘removeHandler’, ‘removeResult’, ‘result’, ‘runner’, ‘signals’, ‘skip’, ‘skipIf’, ‘skipUnless’, ‘suite’, ‘util’]
如下所示:
所有测试用例类继承的基本类。测试行为的最小单位,通过对一些输入输出值的对比来进行测试检查。
TestLoader负责根据各种各样的规则收集测试用例,并把这些测试用例包装在一个TestSuite中。
组织测试用例的实例,支持测试用例的添加和删除,最终将传递给 testRunner进行测试执行。一个test suite由许多个TestCase组成,常见的情形是创建一个TestSuite的实例,然后向TestSuite实例添加测试用例TestCase实例,当所有测试添加之后,可以把TestSuite的实例传递给TextTestRunner,它会按照添加测试用例的顺序逐个去执行测试用例,并手机测试结果
一个test runner类会以文本的形式显示结果,在它们运行时会打印测试名字,发生的错误,并且在测试运行结束之后打印结果概要。将测试用例或测试用例集合聚合起来的集合,批量执行。
由TextTestRunner调用,会显示出格式化的文本。
unittest 模块使用的模式有三种,如下:
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()
4.2 通过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)
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)
源文件widget.py,也即我们要进行单元测试的文件。很简单的一个类,该类可以设置长宽,并且能够获取这两个值。
# /usr/bin/python
# encoding:utf-8
class Widget:
def __init__(self, size=(40, 40)):
self._size = size
def get_size(self):
return self._size
def resize(self, width, height):
if width < 0 or height < 0:
raise ValueError, 'illegal size'
self._size = (width, height)
if __name__ == '__main__':
widget = Widget()
print widget.get_size()
因此,我们可以撰写的单元测试代码文件widgettest.py如下:
# /usr/bin/python
# encoding:utf-8
from widget import Widget
import unittest
class WidgetTestCase(unittest.TestCase):
# 对象的初始化工作可以在setUp()方法中完成
def setUp(self):
self.widget = Widget()
# 对象的资源的释放则可以在tearDown()方法中完成
def tearDown(self):
self.widget.dispose()
self.widget = None
# 对应widget类中的get_size函数测试
def test_size(self):
self.assertEqual(self.widget.get_size(), (40, 40))
# 对应widget类中resize函数的测试
def test_Resize(self):
self.widget.resize(100, 100)
self.assertEqual(self.widget.get_size(), (100, 100))
if __name__ == "__main__":
unittest.main()
其中有两个测试用例,分别为test_size, test_Resize, 在每个测试用例执行的过程中会首先执行setUp,然后执行测试用例,最后执行tearDown操作。每个测试用例的执行遵循相同的模式。
其中要注意的是setUp和tearDown两个,又称为测试装置,setUp完成对象的初始化动作,而tearDown则完成资源的释放之类的操作。
在实际工作中,最常见的实践仍是写一个测试类继承自unittest.TestCase,然后让每一个测试用例名称以test开头,在运行测试集合时使用unittest.main()函数即可。自动化测试便是建立在这样的基础上。
在单元测试进行的同时,就离不开mock模块的存在,初次接触这个概念的时候会有这样的疑问:把要测的东西都模拟掉了还测试什么呢?
但在,实际生产中的项目是非常复杂的,对其进行单元测试的时候,会遇到以下问题:
•接口的依赖
•外部接口调用
•测试环境非常复杂
单元测试应该只针对当前单元进行测试, 所有的内部或外部的依赖应该是稳定的, 已经在别处进行测试过的.使用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
在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(“模块名.函数名”)
在rbd_api.py文件中,有一行代码如下:
ret = OpRBD(pool).flatten(img)
类似这种链式调用的,在前一个函数中抛出异常,要怎么写?如下:
rbdServ.OpRBD = MagicMock()
rbdServ.OpRBD(pool).side_effect = rados.Error(“Error: error connecting to the cluster: error code 24”)
例如在文件rbd_api.py中有全局函数checkpoolstat(pool),它是一个全局函数,这样在进行单元测试的过程中,我们可能需要mock该函数。该函数的具体代码如下:
因此,我们在test_rbd_api.py文件中为该函数撰写单元测试,可以这么做。
在文件开始处导入该rbd_api模块。
import rbd_api as rbdAPI
def test_patchInvalid_Parameter(self):
……
rbdAPI.checkpoolstat.return_value = MGR_COMMON.POOL_STAT_ERROR
即可。
在rbd_api文件中有如下代码行:
ret = OpRBD(pool).flatten(img)
在第一个函数未出现异常,在flatten函数中返回值可以在test_rbd_api.py文件中如下写代码:
rbdServ.OpRBD(pool).snap_rollback = MagicMock(return_value = RBD_COMMON.CODE_EXEC_SUCCESS_MODIFY)
#!/usr/bin/python
import rados
class OpRBD:
def __init__(self):
...
def __del__(self):
...
def resize(self, img, size):
try:
with rbd.Image(self.ioctx, img) as image:
if image.size() < size:
image.resize(size)
else:
return RBD_COMMON.CODE_ARGUMENT_LESS_THAN_ORIGINAL
except rbd.ImageNotFound as exce1
print(exce1)
return RBD_COMMON.CODE_IMAGE_NOT_FOUND
由于是在with子句中要进行mock,在此简单的对with的知识点进行说明:
要使用 with 语句,首先要明白上下文管理器这一概念。有了上下文管理器,with 语句才能工作。
下面是一组与上下文管理器和with 语句有关的概念。
#!/usr/bin/python
import rados
class OpRBD:
def __init__(self):
...
def __del__(self):
...
def resize(self, img, size):
try:
with rbd.Image(self.ioctx, img) as image:
if image.size() < size:
image.resize(size)
else:
return RBD_COMMON.CODE_ARGUMENT_LESS_THAN_ORIGINAL
except rbd.ImageNotFound as exce1
print(exce1)
return RBD_COMMON.CODE_IMAGE_NOT_FOUND
class TestOpRBD(unittest.TestCase):
def setUp(self):
...
def tearDown(self):
...
def test_resize(self):
fake_image = Mock()
fake_image.__enter__ = Mock(return_value = fake_image)
fake_image.__exit__ = Mock(return_value = True)
rbd.Image = Mock(return_value = fake_image)
size = 1073741824L / 2
fake_image.size = Mock(return_value = 1073741824L)
fake_image.resize = Mock(return_value = None)
self.assertEqual(self.opRBD.resize(self.img, size), RBD_COMMON.CODE_ARGUMENT_LESS_THAN_ORIGINAL)
size = 2 * 1073741824L
self.assertEqual(self.opRBD.resize(self.img, size), RBD_COMMON.CODE_EXEC_SUCCESS_MODIFY)
rbd.Image = Mock(side_effect = rbd.ImageNotFound("%s image not found!" %self.img))
self.assertEqual(self.resize(self.img, size), RBD_COMMON.CODE_IMAGE_NOT_FOUND)
在rbd_api文件中有一个OpRados类的内容如下:
#!/usr/bin/python
import rados
class OpRados:
def __init__(self):
self.cluster = rados.Rados(conffile=rconf['conffile'])
self.cluster.connect()
def __del__(self):
self.cluster.shutdown()
def lists(self):
return util.return_format(RBD_COMMON.CODE_EXEC_SUCCESS_GET, "", self.cluster.list_pools())
为该类写单元测试,具体代码如下:
#!/usr/bin/python
import rados
import unittest
from mock import Mock
class TestOpRados(unittest.TestCase):
def setUp(self):
fake_Rados = Mock()
fake_Rados.connect = Mock(return_value = None)
fake_Rados.shutdown = Mock(return_value = None)
fake_Rados.list_pools = Mock(return_value = ["sqh", "sqh1"])
# 注意:此处要使得rados.Rados()调用返回fake_Rados.
# 如果写成rados.Rados = fake_Rados,只能使得self.cluster重新生成一个Mock对象
# 无法有效的控制为fake_Rados所添加的属性。
rados.Rados = Mock(return_value = fake_Rados)
self.opRados = OpRados()
def tearDown(self):
fake_Rados = None
self.opRados = None
def test_list(self):
return_list = ["sqh", "sqh1"]
self.assertEqual(self.opRados.lists(), util.return_format(RBD_COMMON.CODE_EXEC_SUCCESS_GET, "", return_list))
有三个文件:rbd_app.py文件内容如下:
#!/usr/bin/python
from flask imort Flask
from flask.ext import restful
app = Flask(__name__)
api = restful.Api(app)
api_version = "/v1.0"
##
## Actually setup the Api resource ruting here
##
...
api.add_resource(rbd_api.Disk, api_version+"pools//disks/
")
...
if __name__ == __main__:
from gevent.wsgi import WSGIServer
http_server = WSGIServer(('0.0.0.0', 4806), app)
http_server.serve_forever()
rbd_api.py文件中Disk类内容如下:
#!/usr/bin/python
import requests
from flask import request
from flask_restful import reqparse, Resource
class Disk(Resource):
def get(self, pool, img):
ret, rbd_info = DAO_query_rbd_info(pool, img)
...
def post(self, pool, img):
parser = reqparser.RequestParser()
# action: create\copy\clone
parser.add_argument('action', type=str)
parser.add_argument('src_pool', type=str)
parser.add_argument('src_rbd', type=str)
parser.add_argument('src_snap', type=str)
parser.add_argument('size', type=str)
parser.add_argument('unit', type=str)
action = args.get('action')
if action == "create":
size = args.get('size')
unit = args.get('unit')
...
elif action == 'copy':
src_pool = args.get('src_pool')
src_rbd = args.get('src_rbd')
...
else
return ...
def delete(self, pool, img):
...
def put(self, pool, img):
parser = reqparser.RequestParser()
parser.add_argument('rb_snap', type=str)
parser.add_argument('action', type=str)
args = parser.parse_args()
if action == "flatten":
...
elif action == "rollback":
snap = args.get("rb_snap")
...
else:
return ...
def patch(self, pool, img):
parser = reqparse.RequestParser()
parser.add_argument("action", type=srt)
parser.add_argument("name", type=str)
parser.add_argument("size", type=float)
parser.add_argument("unit", type=str)
args = parser.parse_args()
action = args.get('action')
if action == "resize":
size = args.get('size')
unit = args.get('unit')
....
else:
return ...
在test_rbd_api.py中卫Disk类撰写单元测试
from rbd_app import app
class TestDisk(uniitest.TestCase):
def setUp(self):
self.disk = Disk()
self.content_type = "application/json"
...
def tearDown(self):
self.disk = None
self.content_type = None
...
@mock.path.object(DAO_RBDMgr, "DAO_query_rbd_info")
def test_get(self, mock_DAO_query_rbd_info):
rbd_info = {"pool_name":"sqh", "parent":{}, "image_size":1073741824L,...}
mock_DAO_query_rbd_info.return_value = (RBD_COMMON.MONGO_SUCCESS, rbd_info)
with app.test_request_context("/?pool_name=sqh&image_name=sqh001"):
pool = request.args.get('pool_name')
img=request.args.get('image_name')
self.assertEqual(selff.disk.get(pool, img), RBD_COMMON.http_return(RBD_COMMON.MONGO_SUCCESS))
def test_post(self):
pool = "sqh"
img = "sqh001"
path_url = "v1.0/pools/%s/disks%s" % (pool, img)
data = {"action":"create", "size":2, "unit":"GB"}
with app.test_request_context(path_url, data=json.dumps(data), content_type=self.content_type):
self.assertEqual(self.disk.post(pool, img), RBD_COMMON.http_return(RBD_COMMON.CODE_EXEC_SUCCESS_GET))
def test_delete(self):
pool = "sqh"
img = "sqh001"
path_url = "v1.0/pools/%s/disks%s" % (pool, img)
with app.test_request_context(path_url):
self.assertEqual(self.disk.delete(pool, img), RBD_COMMON.http_return(RBD_COMMON.CODE_EXEC_SUCCESS_DELETE))
def test_put(self):
pool = "sqh"
img = "sqh001"
path_url = "v1.0/pools/%s/disks%s" % (pool, img)
data = {"action":"rollback", "rb_snap":"sqh_snap"}
with app.test_request_context(path_url, data=json.dumps(data), content_type=self.content_type):
self.assertEqual(self.disk.put(pool, img), RBD_COMMON.http_return(RBD_COMMON.CODE_EXEC_SUCCESS_FLATTENING))
def test_patch(self):
pool = "sqh"
img = "sqh001"
path_url = 'v1.0/pools/%s/disks/%s' % (pool, img)
size = 1073741824L
data = {"action":"resize", "size":size, "unit":"GB"}
# the action resize
with app.test_request_context(path_url, data=json.dumps(data), content_type=self.content_type):
self.assertEqual(self.disk.patch(pool, img), RBD_COMMON.http_return(RBD_COMMON.CODE_EXEC_SUCCESS_MODIFY))
@mock.path.object(DAO_RBDMgr, "DAO_query_rbd_info")
def test_get(self, mock_DAO_query_rbd_info):
rbd_info = {"pool_name":"sqh", "parent":{}, "image_size":1073741824L,...}
mock_DAO_query_rbd_info.return_value = (RBD_COMMON.MONGO_SUCCESS, rbd_info)
with app.test_request_context("/?pool_name=sqh&image_name=sqh001"):
pool = request.args.get('pool_name')
img=request.args.get('image_name')
self.assertEqual(selff.disk.get(pool, img), RBD_COMMON.http_return(RBD_COMMON.MONGO_SUCCESS))
在为flask接口撰写单元测试时,最关键的是如何传递数据,
我们在setUp函数中初始化Disk类的实例,即self.disk。在传递参数的时候通过调用app.test_request_context(“url”)。因为是restful的get请求,因而我们可以使用?param1=val1¶m2=val2的形式。
在Post请求,我们需要传递动作的类型,即其他参数,在app.test_request_context函数中,传递了字符串url, data字典,HTTP请求头信息。
def test_post(self):
pool = "sqh"
img = "sqh001"
path_url = "v1.0/pools/%s/disks%s" % (pool, img)
data = {"action":"create", "size":2, "unit":"GB"}
with app.test_request_context(path_url, data=json.dumps(data), content_type=self.content_type):
self.assertEqual(self.disk.post(pool, img), RBD_COMMON.http_return(RBD_COMMON.CODE_EXEC_SUCCESS_GET))
如post
如post
如post
因为在python2.*中,mock是单独的模块,因而我们需要单独安装mock模块才能够正常的使用。
因此,我们首先要在rpm find 网站下载好pip,然后通过pip安装mock。
http://rpmfind.net/linux/RPM/epel/7/x86_64/Packages/p/python2-pip-8.1.2-5.el7.noarch.html
依赖于python-setuptools
http://rpmfind.net/linux/RPM/centos/7.4.1708/x86_64/Packages/python-setuptools-0.9.8-7.el7.noarch.html
mock的下载网址:
https://pypi.python.org/pypi/mock/2.0.0
下一章节要用的coverage,测试代码覆盖率,网址在:
https://pypi.python.org/pypi/coverage/4.5.1
在单元测试的过程中,应尽量使得单元都被覆盖过,因而需要使用专门的工具来进行测试代码覆盖率。以test_rbd_api.py为准
测试的步骤如下:
>>> coverage run test_rbd_api.py
>>>coverage report -m path/to/rbd_api.py
即可查看代码覆盖程度。
https://download.csdn.net/download/lk142500/10898459