目录
单元测试的原理
单元测试中的单元可以是一个模块文件, 测试的内容就是模块自身的代码(非导入型代码)是否正确执行. 其中包含了测试代码的正反向逻辑是否正确, 异常能否被正常的触发等程序流. 所以我们会使用伪数据来替代这个单元中所有导入型代码的数据集(函数返回值/数据值).
单元测试的实现
这里使用一个 API 接口模块的单元测试为例.
单元测试文件存储路径: /opt/stack/keystone/keystone/tests/unit
单元测试代码文件的命名规则: “test_moduleName.py”
EXAMPLE:
被测试的模块为 vmware_connects.py, 其单元测试的实现为 test_vmware_connects.py.
在大多数的单元测试文件中都会涉及到以下几个类:
from serviceName import test # 其中 class test.TestCase 是单元测试类的父类
from serviceName.tests.unit.api import fakes # 主要提供 HTTP 请求的相关数据
from serviceName.tests.unit.api.v1 import stubs # 为单元测试类提供伪数据
- 首先, 需要查看 vmware_connects.py 模块中所需要的伪属性数据
因为 vmware_connects.py 是一个 HTTP API 模块, 所以我们可以从数据库中的 vmware_connects 表得知其返回的数据集.
mysql> desc vmware_connects;
+------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
| created_at | datetime | YES | | NULL | |
| updated_at | datetime | YES | | NULL | |
| deleted_at | datetime | YES | | NULL | |
| deleted | tinyint(1) | YES | | NULL | |
| id | varchar(45) | NO | PRI | NULL | |
| ipaddr | varchar(255) | YES | | NULL | |
| username | varchar(255) | YES | | NULL | |
| password | varchar(255) | YES | | NULL | |
| port | int(11) | YES | | NULL | |
| is_vcenter | tinyint(1) | YES | | NULL | |
+------------+--------------+------+-----+---------+-------+
除去基础字段 created_at/updated_at/deleted_at/deleted 之外, 剩下的字段都是会被 vmware_connect 模块中的方法返回的, 所以我们需要在上述的 stubs 模块中为这些属性值设置伪数据.
# tests/unit/api/v1/stubs.py
DEFAULT_VMWCON_ID = "00000000-0000-0000-0000-000000000001"
DEFAULT_VMWCON_IPADDR = "127.0.0.1"
DEFAULT_VMWCON_USERNAME = "root"
DEFAULT_VMWCON_PASSWORD = "vmware"
DEFAULT_VMWCON_PORT = "443"
DEFAULT_VMWCON_ISVCENTER = None
- 然后我们再来看看, 在 vmware_connects 模块中含有那些需要被替换的伪方法数据
EXAMPLE: 在 vmware_connects.VmwareConnectController:show() 中调用了外来模块 vmware_connect_api 的 vmware_connect_get() 方法. 除此之外还实现了 try-catch 语句. 所以我们仍要在 stubs 模块中实现vmware_connect_api.vmware_connect_get()
的伪方法
@wsgi.serializers(xml=VmwareConnectTemplate)
def show(self, req, id):
"""Return data about the given vmware connect."""
context = req.environ['egis.context']
try:
vmware_connect = self.vmware_connect_api.
vmware_connect_get(context, id)
except exception.NotFound as e:
LOG.exception(_LE("Failed to show vmware_connect. id: %(s)s"
"error: %(err)s"),
{'s': id, 'err': six.text_type(e)})
raise exc.HTTPNotFound(explanation=e.msg)
return self.view_builder.show(req, vmware_connect)
在 stubs 模块中实现伪方法之前, 我们先定义一个用于测试 vmware_connects 模块的单元测试类 FakeVmwareConnect, 并且在该类中我们会定义一个方法 fake_vmware_connect()
用于返回当我们正确执行数据库调用时, 所被返回的伪数据.
class FakeVmwareConnect(object):
def fake_vmware_connect(self, kwargs=dict()):
vmware_connect = {
'id': DEFAULT_VMWCON_ID,
'ipaddr': DEFAULT_VMWCON_IPADDR,
'username': DEFAULT_VMWCON_USERNAME,
'password': DEFAULT_VMWCON_PASSWORD,
'port': DEFAULT_VMWCON_PORT
}
vmware_connect.update(kwargs)
return vmware_connect
def fake_vmware_connect_get(self, context, vmware_connect_id):
return self.fake_vmware_connect()
当然, 还需要定义 vmware_connect_api.vmware_connect_get()
的伪方法 fake_vmware_connect_get()
.
def fake_vmware_connect_get(self, context, vmware_connect_id=None):
return self.fake_vmware_connect(vmware_connect_id)
方法 FakeVmwareConnect:fake_vmware_connect_get()
将会替换方法 vmware_connects.VmwareConnectController:show().vmware_connect_api.vmware_connect_get()
并返回之前已经定义好了的 vmware_connect_get()
.
最后还需要定义一个能够触发异常的伪数据, 而且我们可以看出 show() 方法中的 except 语句捕获的是 HttpNotFound 异常. 所以继续在 stubs 模块中定义一个方法 fake_vmware_connect_get_notfound()
.
def fake_vmware_connect_get_notfound(self, context,
vmware_connect_id):
raise exc.NotFound(vmware_connect_id)
- 这样的话, 就针对
vmware_connects.VmwareConnectController:show()
来说所需要的伪数据都准备好了. 接下来就可以实现 test_vmware_connects.py 了.
import webob
from serviceName import test
from serviceName.tests.unit.api import fakes
from serviceName.tests.unit.api.v1 import stubs
from serviceName.api.v1 import vmware_connects
from serviceName.recover.virt.drivers.vmware.vmware_connects import api
HTTP_PASH = '/v1/vmware_connects'
class VmwareConnectAPITest(test.TestCase):
def setUp(self):
super(VmwareConnectAPITest, self).setUp()
self.controller = vmware_connects.VmwareConnectController()
self.fake_vmware_connect = stubs.FakeVmwareConnect()
# 将 api.API:vmware_connect_get() 替换成 stubs.FakeVmwareConnect:fake_vmware_connect_get()
# 这一条语句非常重要, 指定了被测单元中的导入数据与伪数据间替换的映射关系.
self.stubs.Set(api.API, 'vmware_connect_get',
self.fake_vmware_connect.fake_vmware_connect_get)
def _vmware_connect_in_request_body(
self,
id=stubs.DEFAULT_VMWCON_ID,
ipaddr=stubs.DEFAULT_VMWCON_IPADDR,
username=stubs.DEFAULT_VMWCON_USERNAME,
password=stubs.DEFAULT_VMWCON_PASSWORD,
port=stubs.DEFAULT_VMWCON_PORT,
is_vcenter=stubs.DEFAULT_VMWCON_ISVCENTER):
"""这个方法用于模拟当 HTTP Request 调用 API 时, 所传入的数据."""
vmware_connect = {'id': id,
'ipaddr': ipaddr,
'username': username,
'password': password,
'port': port,
'is_vcenter': is_vcenter,
'created_at': None,
'updated_at': None}
return vmware_connect
def _expected_vmware_connect_from_controller(
self,
id=stubs.DEFAULT_VMWCON_ID,
ipaddr=stubs.DEFAULT_VMWCON_IPADDR,
username=stubs.DEFAULT_VMWCON_USERNAME,
password=stubs.DEFAULT_VMWCON_PASSWORD,
port=stubs.DEFAULT_VMWCON_PORT,
is_vcenter=stubs.DEFAULT_VMWCON_ISVCENTER,
created_at=None,
updated_at=None):
"""这个方法用于模拟预期希望从 vmware_connects 模块中返回的数据."""
vmware_connect = {'vmware_connect':
{'id': id,
'ipaddr': ipaddr,
'username': username,
'password': password,
'port': port,
'is_vcenter': is_vcenter,
'created_at': created_at,
'updated_at': updated_at}}
return vmware_connect
def test_vmware_connect_show(self):
# 模拟 Http 请求的所发送的相关信息
req = fakes.HTTPRequest.blank(''.join([HTTP_PASH,
stubs.DEFAULT_VMWCON_ID]))
# 传入伪数据实参来调用 vmware_connects.VmwareConnectController:show() 方法, 并且该方法中所有的导入型数据都已经使用伪数据来替换了. 所以我们可以得出该方法实际返回的结果.
res_dict = self.controller.show(req, stubs.DEFAULT_VMWCON_ID)
# 预期返回的结果, 这个伪数据是由我们人为的去限定的
expected = self._expected_vmware_connect_from_controller(
id=stubs.DEFAULT_VMWCON_ID)
# 比较实际返回的结构和预期返回的结构是否相同, 如果相同则通过测试, 反之, 则失败.
# 由于无论是预期返回的结果还是实际返回的结果, 都是以在 stubs 模块中定义的伪属性数据为基础的, 所以只要在保证 show() 方法的正常执行, 那么两者应该是相同的.
self.assertEqual(expected, res_dict)
def test_vmware_connect_show_notfound(self):
# 在这一个方法中, 我们为了要触发异常, 所以我们应该将api.API:vmware_connect_get() 替换成 stubs.FakeVmwareConnect:fake_vmware_connect_get_notfound()
self.stubs.Set(
api.API, 'vmware_connect_get',
self.fake_vmware_connect.fake_vmware_connect_get_notfound)
req = fakes.HTTPRequest.blank(''.join([HTTP_PASH, '/1000']))
# 验证是否有正确的重新触发异常, 第二个参数为实际的 show() 方法, 还需要为 show() 传入所需的两个参数, 否则会触发错误.
self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
req, 1000)
- 现在我们可以执行单元测试的指令了
sudo tox -e py27
如果通过了单元测试的话, 最后会 Output: Successfully!
NOTE: 编写单元测试用例的时候, 默认是不能通过 pdb 来调试的. 如果希望通过 pdb 来调试代码的话需要执行以下步骤:
sudo pip install -e . -r test-requirements.txt -r requirements.txt
在希望 DEBUG 的地方打上断点之后运行:
python -m testtools.run serviceName.tests.unit.api.v1.test_vmware_connects
就可以进入调试 console 了.
最后
这只是一个 Openstack 项目中非常简单的一个 HTTP API 单元测试, 我们最重要的是要理解单元测试的原理及其存在的意义.
原理: 确保被测试的单元模块中的导入型数据都被替换成伪数据, 以此来保证单元的独立性. 并在此独立的条件下确保单元正确的逻辑和正确的异常处理.
意义: 单元测试能够保证项目中的每一个模块在被修改后还能保持其原始的标准, 如果在修改了一个模块后不能保证其标准的话, 当我再次执行单元测试时, 就会报错. 这些标准是非常重要的, 是一个复杂的项目能够正常运行的基础.