偷梁换柱:使用mock.patch辅助python单元测试
最近在搞软工项目的后端测试,重新复习了一下python的mock.patch,并用它简化了对一些复杂逻辑的测试,在此记录
问题描述
本组的项目比较特殊,设计对教务网站的模拟登陆与信息爬取,同时不少接口会有发送邮件的side-effect。在自动化测试时,由于这两个功能的行为与生产环境的真实数据(用户的教务账号、邮箱地址)耦合,需要想办法设计专门的测试流程。容易想到的比较简单的思路有:
- 为相关接口开一个标记测试的布尔值参数,在测试时传入,屏蔽邮件发送/爬取教务网站的相关逻辑,并为邮件/爬虫设计单独的测试逻辑,将其与网站主要逻辑的测试解耦。好处是实现简单,缺点是需要修改正常的接口逻辑,不符合开闭原则,且若处理不当易导致安全隐患。
- 提供一个专门的测试账号,在测试时使用该账号测试相关功能。优点是不需要修改接口逻辑,问题是对于爬取教务这种需求,提供的账号是真实的学生账号,自动测试时可预见的频繁密集的数据请求可能会影响账号的正常使用。
综合上述两个思路,不难想到去寻找一种可以跳过邮件发送/网站爬取逻辑但又不需要修改后端代码逻辑的方法。由于python是解释型语言,在程序运行时可以非常方便地将某一段代码进行动态替换,所以只要在测试时将发送邮件的函数/方法替换成一个“假”函数即可。借助importlib等手段不难实现,但工作量稍大,实际上python已经为我们提供了unittest.mock.patch来满足这种需求。
基本介绍
详细使用请见官方文档
一篇更简明的介绍性质的教程是An Introduction to Mocking in Python
这里总结一些快速上手的要点
使用方式:装饰器或上下文管理器
首先我们给出一个玩具函数func_to_test,这个函数接收两个参数和一个可选参数,返回两个参数的加和,并打印可选参数的值
# author : Mistariano (hdl730@163.com)
# file path : pack1/my_module.py
# module name : pack1.my_module
def verbose_adder(arg1, arg2, kwarg1='default'):
print(kwarg1) # side-effect
return arg1 + arg2 # post-condition
def func_to_test():
return verbose_adder(10, 10)
现在我们希望借助patch,hack掉verbose_adder这个函数。希望无论测试时func_to_test实际传给verbose_adder的参数是什么,其返回值都为3,同时输出一行特定的信息。
下述两种写法都是可行的
# author : Mistariano (hdl730@163.com)
from pack1.my_module import func_to_test
from unittest import mock
def print_test_info(*args, **kwargs):
print('this is a microphone check.')
print('arguments:', args, kwargs)
return mock.DEFAULT # NOTICE here
@mock.patch('pack1.my_module.verbose_adder')
def test_func_to_test__decorator(mock_obj):
mock_obj.return_value = 3
mock_obj.side_effect = print_test_info
assert func_to_test() == 3
def test_func_to_test__context():
with mock.patch('pack1.my_module.verbose_adder') as mock_obj:
mock_obj.return_value = 3
mock_obj.side_effect = print_test_info
assert func_to_test() == 3
可以看到,mock.patch可以以函数装饰器的方式或上下文管理器的方式使用,前者需要被装饰的函数提供一个额外的参数接收mock对象实例mock_obj,后者则会将mock对象实例作为上下文管理器的返回值。当然,直接将其作为函数调用也是可取的,但个人并不推荐,这里不详细讨论。
通过为mock_obj指定返回值(可选的)与副作用(也是可选的)来定制mock函数的行为,从而实现对原函数/方法的动态覆盖
注意到用来作为mock对象side_effect的回调函数返回值是mock.DEFAULT,这样写是为了避免覆盖另行制定的return_value
应该给哪个函数打patch
Mock an item where it is used, not where it came from.
python的加载机制很有意思。对于一个函数,如果mock中指定的模块路径是它定义的地方(而不是实际被调用的地方),则mock可能无法成功覆盖已经加载了这个函数的其它模块
对这个问题的详细解释可以参考官方文档,同时这个Stackoverflow提问给出了一些例子,有助于进一步理解。
实战
这里直接给出本组软工代码中使用patch覆盖邮件发送及教务爬取的代码段
from django.test import TestCase
from unittest import mock
# ...
class ViewTestCases(TestCase):
# ...
@staticmethod
def mock_mail_send(*args, **kwargs):
print('sending mock mail.. args:', args, kwargs)
return mock.DEFAULT
@staticmethod
def mock_update_from_course(*args, **kwargs):
print('mock updating course... args:', args, kwargs)
return mock.DEFAULT
def _test_req_context(self, func, exp_code, auth_required):
def test_req_wrapper(*args, **kwargs):
token = None if not self._user_data else self._user_data['token']
with mock.patch('ddl_killer.utils.sendmail.YAG.send') as mail_obj:
mail_obj.side_effect = self.mock_mail_send
with mock.patch('ddl_killer.views.updateFromCourse') as mock_course:
mock_course.side_effect = self.mock_update_from_course
mock_course.return_value = self.TEST_COURSE
if auth_required:
r_data = func(*args, HTTP_AUTHORIZATION=None, **kwargs).json()
self.assertEqual(r_data['code'], 401, r_data)
r_data = func(*args, HTTP_AUTHORIZATION=token, **kwargs).json()
self.assertEqual(r_data['code'], exp_code)
return r_data
return test_req_wrapper
def post(self, *args, exp_code=200, auth_required=True, **kwargs):
return self._test_req_context(self._client.post, exp_code, auth_required)(*args, **kwargs)
def get(self, *args, exp_code=200, auth_required=True, **kwargs):
return self._test_req_context(self._client.get, exp_code, auth_required)(*args, **kwargs)
def _login(self):
if self._user_data is None:
print('logging...')
r = self.post('/api/login',
{'uid': self.TEST_USER_ID,
'password': self.password_encrypt},
auth_required=False)
self._user_data = r
def test_show_user(self):
self._login()
data = self.post('/api/user/{}/info'.format(self.TEST_USER_ID))
self.assertEqual(data['uid'], self.TEST_USER_ID)
self.assertEqual(data['name'], self.TEST_USER_NAME)
self.assertEqual(data['email'], self.TEST_USER_EMAIL)
def test_user_login_not_activated(self):
self._user_orm.is_active = False
self._user_orm.save()
r = self.post('/api/login', {'uid': self.TEST_USER_ID,
'password': self.password_encrypt},
exp_code=400,
auth_required=False)
self._user_orm.is_active = True
self._user_orm.save()
def test_edit_user(self):
self._login()
data = self.post('/api/modify', {
'uid': self.TEST_USER_ID,
'name': self.TEST_USER_NAME,
'password': '',
'email': 'tmp_email@mail.com'
})
self.assertEqual(User.objects.get(uid=self.TEST_USER_ID).email,
'tmp_email@mail.com')
self._user_orm.email = self.TEST_USER_EMAIL
self._user_orm.save()
# ...
偷梁换柱:使用mock.patch辅助python单元测试的更多相关文章
- Python单元测试和Mock测试
单元测试 测试可以保证你的代码在一系列给定条件下正常工作 测试允许人们确保对代码的改动不会破坏现有的功能 测试迫使人们在不寻常条件的情况下思考代码,这可能会揭示出逻辑错误 良好的测试要求模块化,解耦代 ...
- 使用Python的Mock库进行PySpark单元测试
测试是软件开发中的基础工作,它经常被数据开发者忽视,但是它很重要.在本文中会展示如何使用Python的uniittest.mock库对一段PySpark代码进行测试.笔者会从数据科学家的视角来进行描述 ...
- python笔记24-unittest单元测试之mock.patch
前言 上一篇python笔记23-unittest单元测试之mock对mock已经有初步的认识, 本篇继续介绍mock里面另一种实现方式,patch装饰器的使用,patch() 作为函数装饰器,为您创 ...
- python文档2-unittest单元测试之mock.patch
介绍mock里面另一种实现方式,patch装饰器的使用,patch() 作为函数装饰器,为您创建模拟并将其传递到装饰函数 patch简介 1.unittest.mock.patch(target,ne ...
- The Hacker's Guide To Python 单元测试
The Hacker's Guide To Python 单元测试 基本方式 python中提供了非常简单的单元测试方式,利用nose包中的nosetests命令可以实现简单的批量测试. 安装nose ...
- 利用Python中的mock库对Python代码进行模拟测试
这篇文章主要介绍了利用Python中的mock库对Python代码进行模拟测试,mock库自从Python3.3依赖成为了Python的内置库,本文也等于介绍了该库的用法,需要的朋友可以参考下 ...
- [python] python单元测试经验总结
python写单元大多数都会用到unittest和mock,测试代码覆盖率都会用到coverage,最后再用nose把所有的东西都串起来,这样每次出版本,都能把整个项目的单元测试都运行一遍. Unit ...
- 【转】利用Python中的mock库对Python代码进行模拟测试
出处 https://www.toptal.com/python/an-introduction-to-mocking-in-python http://www.oschina.net/transla ...
- [译]PyUnit—Python单元测试框架(1)
1. 原文及参考资料 原文链接:http://docs.python.org/2/library/unittest.html# 参考文档: http://pyunit.sourceforge.net/ ...
随机推荐
- linux screen的用法
今天使用vps时,起了一个http服务,因为需要用nc接收流量,就要关闭http服务,再去用nc接收流量就接收不到,请教了师傅,这里需要用到screen创建一个会话,就能http服务跟nc同时进行. ...
- effective解读-第八条 避免使用finalizer和Cleaner
java9之前finalizer,java9使用cleaner代替了finalizer.相比finalizer,cleaner(它存在于一个独立类Cleaner中,需要时候注入到对应类中即可)不会污染 ...
- 关于HashMap的一些思考
一.HashMap的负载因子的作用 当 HashMap 中的元素个数(包含链表.红黑树上的元素)达到数组长度的0.75倍的时候,开始扩容. 二.HashMap的负载因子为什么是0.75 主要是为了 ...
- Python3基础之构建setup.py
技术背景 在上一篇博客中,我们介绍了如何使用pyinstaller将python项目打包成一个可执行文件,并且放在系统目录下,让系统用户可以直接识别到我们构造的项目.而python项目中常见的setu ...
- Android 之 SimpleAdapter 学习笔记
•SimpleAdapter简介 simpleAdapter 的扩展性最好,可以定义各种各样的布局出来: 可以放上ImageView(图片),还可以放上Button(按钮),CheckBox(复选框) ...
- 实现FTP+PAM+MySQL环境,批量配置虚拟用户
实现FTP+PAM+MySQL环境,批量配置虚拟用户 搭建环境: CentOS6.5或CentOS6.7 [root@vhost3 ~]# uname -a Linux vhost3 2.6.32-5 ...
- 【3.0 递归 Recursion 02】
[递归:阶乘] 1.寻找基本情况 对于阶乘而言,最基本的情况就是0!和1!,二者的结果都是1 我们不妨现在方法中写下这个情况,帮助我们跳出递归 if(i<=1){ return 1 ; } 接下 ...
- ES系列(二):基于多播的集群发现实现原理解析
ES作用超强悍的搜索引擎,除了需要具有齐全的功能支持,超高的性能,还必须要有任意扩展的能力.一定程度上,它是一个大数据产品.而要做扩展性,集群自然少不了.然而单独的集群又是不够的,能够做的事情太少,所 ...
- 防火墙双出口环境下私网用户通过NAPT访问Internet
组网图形 组网需求 如图1所示,某企业在网络边界处部署了FW作为安全网关,并分别从运营商ISP1和ISP2处购买了宽带上网服务,实现内部网络接入Internet的需求. 具体需求如下: 研发部门和市场 ...
- Salesforce学习之路(七)Visualforce结合Reports展示图表
Salesforce作为一款CRM系统,个人觉得最重要的环境便是在于数据的展示和联动,而Salesforce也本身提供了相当强大的功能,Report在展示图表的方面十分强大,前段时间更是宣布以157亿 ...