最近在搞软工项目的后端测试,重新复习了一下python的mock.patch,并用它简化了对一些复杂逻辑的测试,在此记录

问题描述

本组的项目比较特殊,设计对教务网站的模拟登陆与信息爬取,同时不少接口会有发送邮件的side-effect。在自动化测试时,由于这两个功能的行为与生产环境的真实数据(用户的教务账号、邮箱地址)耦合,需要想办法设计专门的测试流程。容易想到的比较简单的思路有:

  1. 为相关接口开一个标记测试的布尔值参数,在测试时传入,屏蔽邮件发送/爬取教务网站的相关逻辑,并为邮件/爬虫设计单独的测试逻辑,将其与网站主要逻辑的测试解耦。好处是实现简单,缺点是需要修改正常的接口逻辑,不符合开闭原则,且若处理不当易导致安全隐患。
  2. 提供一个专门的测试账号,在测试时使用该账号测试相关功能。优点是不需要修改接口逻辑,问题是对于爬取教务这种需求,提供的账号是真实的学生账号,自动测试时可预见的频繁密集的数据请求可能会影响账号的正常使用。

综合上述两个思路,不难想到去寻找一种可以跳过邮件发送/网站爬取逻辑但又不需要修改后端代码逻辑的方法。由于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() # ...

最新文章

  1. atitit..代码生成流程图 流程图绘制解决方案 java  c#.net  php v2
  2. ubuntu14.04 的网络配置
  3. PAT (Basic Level) Practise:1007. 素数对猜想
  4. Android公共库(缓存 下拉ListView 下载管理Pro 静默安装 root运行 Java公共类)
  5. 优秀android开源项目与解决方案推荐
  6. leetcode2 Two Sum II – Input array is sorted
  7. vs2013搭建团队版本控制 TFS、SVN
  8. CentOS下JAVA WEB 环境搭建
  9. java 多线程,T1 T2 T3 顺序执行
  10. linux搭建django项目基本步骤
  11. Data - Spark简介
  12. Centos7防火墙快速开放端口配置方法
  13. 非关系型数据库(nosql)介绍
  14. error C4430: 缺少类型说明符 - 假定为 int。注意: C++ 不支持默认 int 错误的解决方法
  15. 【Canal源码分析】配置项
  16. myeclipse优化 Maven
  17. UGUI——重写Image类实现进度条
  18. Python format 格式化函数 格式化字符串
  19. asp.net core mvc视频A:笔记3-6.视图数据共享之session/cache
  20. prop()方法和attr()方法以及区别

热门文章

  1. FFMPEG编译问题记录
  2. PAT (Advanced Level) Practice 1027 Colors in Mars (20 分) 凌宸1642
  3. CQGUI框架之样式管理
  4. 什么是一致性hash?
  5. 从HotSpot VM源码看字符串常量池(StringTable)和intern()方法
  6. 字节、位、bit、byte、KB、B、字符之间的关系
  7. 【长文】Spring学习笔记(七):Mybatis映射器+动态SQL
  8. Flowable中的Service
  9. 案例分析——Who is the king of handwriting notes?
  10. 05- web网站链接测试与XENU工具使用