谈一次单元测试驱动代码重构
目前团队并没有QA岗,而且在很长一段时间内,可能也不会设立QA岗,所以我们需要RD保证代码的质量。而鉴于人类天生的“惰性”,很多时候质量完全依赖于作者的能力以及职业素质。于是我在团队内推动单元测试,并要求提升测试覆盖率。虽然单元测试不能“根治”bug,但是它可以驱使代码结构简洁可测,为提升测试代码覆盖率奠定基础,从而可以有效降低bug率。(转载请指明出于breaksoftware的csdn博客)
以下我将以工作中一个实际例子讲解如何将一个不可测代码变成更加合理且可测代码。
class CheckLinkRequest:def execute(self):try:db = Db()app_links = db.query(AppLinks).filter(AppLinks.valid == True).all()LOG_DEBUG('app links data is {0}'.format(app_links))data_list = []if app_links:for _ in app_links:LOG_DEBUG('app links data is {0}'.format(_))user = db.query(AccountUser.email).filter(AccountUser.valid == True, AccountUser.id == _.user_id).all()LOG_DEBUG('user email is {0}'.format(user))data_list.append({"source": simplejson.dumps({'url': _.app_link, 'id': _.id, 'email': user[0][0]})})LOG_DEBUG('data list is {0}'.format(data_list))except Exception as e:LOG_ERROR('app link error {0}'.format(e))return JsonFuncResponser({'data': data_list})else:return JsonFuncResponser({'data': data_list})
这段代码大致意思是:
- 从AppLinks表中检索出所有有效数据(第5行)
- 遍历1中结果,查询每个信息对应的email(第11,12行)
- 将1中渠道的link信息和2中渠道的email信息组装成一条记录(第14,15行)
这段代码有好几个问题:
- 如果异常发生在第7行之前,执行到第19行时由于data_list未声明而被使用,将抛出异常
- 两处查询数据库可能产生的异常很不方便测试
- 第8行判断没有必要,而且造成一层嵌套。如果返回的数组,则可以进入异常处理;如果返回空数组,第21行也能正确处理。
- 第15行想当然的认为user是个二维数组,从而导致抛出异常
我们开始着手对这段代码进行改造。
依据“职责单一原则”,execute方法包含了太多功能,我们需要将其进行拆解重组:
class CheckLinkRequest:def __init__(self):self._db = Nonedef _init_db(self):if not self._db:self._db = Db()def _get_all_valid_applinks(self):self._init_db()return self._db.query_list(AppLinks.app_link, AppLinks.user_id, AppLinks.id).filter(AppLinks.valid == True).all()def _get_email_by_user_id(self, user_id):self._init_db()user = self._db.query_list(AccountUser.email).filter(AccountUser.valid == True, AccountUser.id == user_id).first()return user.emaildef _email_empty(self, user_id):LOG_WARNING("need to set email for user{0}".format(user_id))def _execute_with_exception(self):data_list = []app_links = self._get_all_valid_applinks()LOG_DEBUG('app links data is {0}'.format(app_links))for _ in app_links:LOG_DEBUG('app links data is {0}'.format(_))email = self._get_email_by_user_id(_.user_id)if not email:self._email_empty(_.user_id)LOG_DEBUG('user email is {0}'.format(email))data_list.append({"source": simplejson.dumps({'url': _.app_link, 'id': _.id, 'email': email})})return data_listdef execute(self):data_list = []try:data_list = self._execute_with_exception()except Exception as e:LOG_ERROR('app link error {0}'.format(e))return JsonFuncResponser({'data': data_list})
在原代码中Db对象是可被重用的,而修改后我们需要在不同成员函数中使用到它,所以将其提升成成员变量。
没有在构造函数中直接构造Db对象,是因为希望构造函数足够简单,只是进行一些数值型的构造,而不发生诸如“连接数据库”这类比较重的操作。
这样为了不频繁构建DB对象,我们设计了_init_db方法,同时在使用Db的地方都用其初始化一下。
我们修复了原代码中对user结构的“预设”隐患(直接取用了user[0[0]),同时也给我们暴露出“如果email为空该怎么办?”业务相关的问题。于是我们引入_email_empty方法来处理该业务性问题。
最后我们将execute封装出一个抛出异常的版本和无异常的版本。
经过改造后,代码结构变得清晰,execute函数职责也变得清晰。
分析这段代码,我们可以列出大致的测试点:
_get_all_valid_applinks/_get_email_by_user_id抛出异常
_get_all_valid_applinks/_get_email_by_user_id返回None
_get_all_valid_applinks返回空List
_get_all_valid_applinks返回的不是List
明确好这些测试点,我们开始编写单元测试代码
监测抛出异常
我们使用mock技术,在第9、10和21、22分别让,分别让执行_get_all_valid_applinks、_get_email_by_user_id时抛出异常
class TestCheckLinkRequest():def setup_class(self):passdef teardown_class(self):passdef test_get_all_valid_applinks_raise_exception(self, mocker):mocker.patch('basic_insights.check_link_request.CheckLinkRequest._get_all_valid_applinks', side_effect=Exception)t = CheckLinkRequest()with pytest.raises(Exception):t._execute_with_exception()r = t.execute()assert(r.is_same_data(JsonFuncResponser({'data': []})))def test_get_email_by_user_id_raise_exception(self, mocker):mocker.patch('basic_insights.check_link_request.CheckLinkRequest._get_email_by_user_id', side_effect=Exception)t = CheckLinkRequest()with pytest.raises(Exception):t._execute_with_exception()r = t.execute()assert(r.is_same_data(JsonFuncResponser({'data': []})))
然后在14、15和26、27行监测调用_execute_with_exception时会抛出异常。
最后17和29行执行无抛出异常版本的execute,并在之后判断返回值是否符合预期。
监测返回None
我们先看_get_all_valid_applinks在返回None时的单元测试。
def test_get_all_valid_applinks_return_none(self, mocker):mocker.patch('basic_insights.check_link_request.CheckLinkRequest._get_all_valid_applinks', return_value=None)t = CheckLinkRequest()with pytest.raises(Exception):t._execute_with_exception()r = t.execute()assert(r.is_same_data(JsonFuncResponser({'data': []})))
我们在2、3行让_get_all_valid_applinks返回None。由于遍历None会抛出异常,所以7、8行将监测异常抛出。其他监测和之前相同。
_get_email_by_user_id返回None的话,它不会抛出异常,所以我们直接调用了_execute_with_exception而不期待其异常。由于email是空,将会触发_email_empty执行,于是我们在第5行mock了一下该对象的该函数,然后在第11行确定该函数被调用了。
def test_get_email_by_user_id_return_none(self, mocker):mocker.patch('basic_insights.check_link_request.CheckLinkRequest._get_email_by_user_id', return_value=None)t = CheckLinkRequest()mocker_email_empty = mocker.patch.object(t, '_email_empty')t._execute_with_exception()r = t.execute()assert(False == r.is_same_data(JsonFuncResponser({'data': []})))assert(mocker_email_empty.called)
返回空List/dict
_get_all_valid_applinks返回空List或者dict,其返回值结果集也将是空。
def test_get_all_valid_applinks_return_empty(self, mocker):mocker.patch('basic_insights.check_link_request.CheckLinkRequest._get_all_valid_applinks', return_value=[])t = CheckLinkRequest()t._execute_with_exception()r = t.execute()assert(r.is_same_data(JsonFuncResponser({'data': []})))def test_get_all_valid_applinks_return_obj(self, mocker):mocker.patch('basic_insights.check_link_request.CheckLinkRequest._get_all_valid_applinks', return_value={})t = CheckLinkRequest()t._execute_with_exception()r = t.execute()assert(r.is_same_data(JsonFuncResponser({'data': []})))
最后我们监测一个正常的情况
def test_result(self, mocker):ret = [{'url': "www.1.com", 'id': 1, 'email': "1@1.com"},{'url': "www.2.com", 'id': 2, 'email': ""}]app_links = []for _ in ret:app_links.append(AppLinks(app_link = _["url"], user_id = _["id"], id = _["id"]))mocker.patch('basic_insights.check_link_request.CheckLinkRequest._get_all_valid_applinks', return_value=app_links)def mocker_get_email_by_user_id(id):emails = {1: "1@1.com"}if id in emails:return emails[id]else:return ""mocker.patch('basic_insights.check_link_request.CheckLinkRequest._get_email_by_user_id', wraps=mocker_get_email_by_user_id)t = CheckLinkRequest()mocker_email_empty = mocker.patch.object(t, '_email_empty')t._execute_with_exception()r = t.execute()r_list = []for _ in r.json()['data']:r_list.append(simplejson.loads(_["source"]))assert(r_list == ret)assert(mocker_email_empty.call_count == 2)
这段代码我们使用mocker_get_email_by_user_id替换了CheckLinkRequest的_get_email_by_user_id,从而我们可以干涉其内部执行。这也是一种非常常用的设计。
相关文章:

新机会在广州拓波
公司简介广州拓波软件科技有限公司的前身为 Turbomail工作室,由广州华工信息软件(集团)有限公司于2002 年成立,是一家专业研发电子邮件系统、企业即时通信和企业短信的开发组织,2005年TurboMail工作室正式发布1.0.2版本…
关于正则表达式,这篇都讲清楚了
作者 | 猪哥来源 | 裸睡的猪(ID:rgznai100)目前越来越多的网站、编辑器、编程语言都已支持一种叫“正则表达式”的字符串查找“公式”,有过编程经验的同学都应该了解正则表达式(Regular Expression 简写regex)是什么东…

MJExtension简介
MJExtension简介 前言:关于MJExtension更多的使用,可以到github网站上根据详述学习。 字典转模型比较流行的第三方框架 Mantle所有模型都必须继承自MTModel JSONModel所有模型都必须继承自JSONModel MJExtension不需要强制继承任何其他类 框架需要考虑的…

Discuz!常用函数解析(续)
/*** 产生随机码* param $length - 要多长* param $numberic - 数字还是字符串* return 返回字符串*/function random($length, $numeric 0) {PHP_VERSION < 4.2.0 && mt_srand((double)microtime() * 1000000);if($numeric) {$hash sprintf(%0.$length.d, mt_ran…
基于新型忆阻器的存内计算原理、研究和挑战
作者 | 林钰登、高滨、王小虎、钱鹤、吴华强来源 | 《微纳电子与智能制造》期刊引言过去半个世纪以来 ,芯片计算性能的提高主要依赖于场效应晶体管尺寸的缩小。随着特征尺寸的减小 ,器件的制备成本和制造工艺难度不断增加 ,芯 片性能的提升愈…

3、JPA一些常用的注解
常用注解有下面这些: ①:Entity、Table、Id、GeneratedValue、Column、Basic ②:Transient 用于忽略某个属性,而不对该属性进行持久化操作 ③:Temporal 一、第①组注解 Entity 标注用于实体类声明语句之前,…

实战域树部署,Active Directory系列之十九
实战子域部署<?xml:namespace prefix o ns "urn:schemas-microsoft-com:office:office" />域树是Active Directory针对NT4的传统域模型所进行的重要改进。在NT4时代的域模型中,每个域都要使用没有层次结构的NETBIOS名称,而且域和域之…
黑科技抗疫,Python开发者大集结!
2020年初,突如其来的新型冠状病毒肺炎打乱了所有人的节奏,但社会各界迅速团结起来,为抗击疫情贡献出自己的力量。除了捐款捐物外,很多科技公司运用5G、大数据、AI、云计算等新互联网技术,以科技的手段助力抗疫…

Inplayable技术分享
Inplayable技术分享运维设计模式Web安全工具语言python运维 《aws lambda 通过codebuild上线踩坑指南之 lambda 进程被占用 status error 255》《google pay 配置sub/pub回调》《AWS攻略——使用CodeCommit托管代码》《AWS攻略——使用S3托管静态网页》《AWS攻略——使用CodeB…

将数组A中的内容和数组B中的内容进行交换(数组一样大)
#include <stdio.h>int main() {int arr1[10]{1,2,3,4,5,11,14,16,17,12};int arr2[10]{0,6,7,8,9,15,21,18,19,13};int arr3[10];int i0;for(i0;i<sizeof(arr1)/sizeof(arr1[0]);i){arr3[i]arr1[i];arr1[i]arr2[i];arr2[i]arr3[i];//不定义第三个变量的两种种方法&am…

***必备工具
***必备工具一、扫描工具 X-scan 3.1 焦点出的扫描器,国内最优秀的安全扫描软件之一!非常专业的一个扫描器! X-way 2.5 这也上一个非常不错的扫描器哦!功能非常多!使用也不难,***必备工具! SuperScan 3.0 强大的TCP 端口扫描器、Ping 和域名解析器! Namp 3.5 这个就…
通过评估假设行为来学习人类目标
来源| deepmind编译| 武明利,责编| Carol出品 | AI科技大本营(ID:rgznai100)当我们在现实世界中训练强化学习(RL)代理时,我们不会希望它们探索不安全的状态,例如将一个移动机器人开进…

ReactiveCocoa入门-part2
ReactiveCocoa是一个框架,它能让你在iOS应用中使用函数响应式编程(FRP)技术。在本系列教程的第一部分中,你学到了如何将标准的动作与事件处理逻辑替换为发送事件流的信号。你还学到了如何转换、分割和聚合这些信号。 在本系列教程…

VirtualBox虚拟机安装RedHat7.3编译Linux0.01内核
引子 由于需要编译linux0.01内核,而目前的linux版本太高需要降低gcc版本等等,需要做不少调整非常不方便。 所以,直接安装RedHat7.3,这样就好编译linux0.01的内核了。 但是,安装RedHat7.3需要注意一些问题。 下载老…
远程办公是巨头游戏?十倍扩容,他们如何做到百万级并发流量
疫情发生后,除了Zoom这样深耕视频会议多年的软件,钉钉、企业微信、飞书等一大批互联网巨头也开通了免费服务,凭借着自身庞大的资源四处招揽用户。 据说,远程办公工具是2020年的第一个风口。 疫情发生后,除了Zoom这样深…

linux下使用sort命令升序、降序、随机及组合方式排序方法
示例文件:####################################################序号 优先级 字段1 字段21 5 abc def2 5 ae3 wff6 4 l…

mysql数据库备份、恢复文档
说明:为了加强线上数据库安全,避免研发人员误操作造成数据的丢失,制作本文档。一线运维人员可以参考!一、数据备份:专用数据库备份服务器,定时对数据库进行热备、冷备,即主从设置、mysqldump冷备、mysql-bin-log日志备…

Linux环境ddd安装与使用
ddd是一个优秀的调试器,安装ddd破费周折 必须安装x开发环境 1.下载 http://ftp.gnu.org/gnu/ddd/,下载最新的ddd-3.3.12.tar.gz # wget http://ftp.gnu.org/gnu/ddd/ddd-3.3.12.tar.gz # tar zxvf ddd-3.3.12.tar.gz # cd ddd-3.3.12/ 2.配置 # ./…
华为诺亚、北大提出GhostNet,使用线性变换生成特征图,准确率超MobileNet v3 | CVPR 2020...
作者 | Kai Han, Yunhe Wang等编译 | Conv出品 | AI科技大本营(rgznai100)受限于内存空间和计算资源,将卷积神经网络部署到嵌入式设备中会比较困难。CNNs中特征图的冗余性是保证其成功的关键,但是在神经网络的结构设计中却鲜有研究…

pap和chap交叉认证
pap和chap交叉认证:R1启动pap,R2启动chap。R1上的配置:Router>enRouter#config tRouter(config)#enable s ciacoRouter(config)#line c 0Router(config-line)#pass ciacoRouter(config-line)#loginRouter(config-line)#logging syRouter(c…

如何在App中实现朋友圈功能之二快速实现用户信息的自定义——箭扣科技Arrownock...
如何在App中实现朋友圈功能之二快速实现用户信息的自定义自我关联社交元素:anSocial中很多的社交元素API,如帖子(Post)、相册(Album)、文件(File)等,这些API的可选参数中…

使用cat /proc/进程id/maps 查看进程内存映射
proc/<PID>/maps 查看进程的虚拟地址空间是如何使用的。 该文件有6列,分别为: 地址:库在进程里地址范围 权限:虚拟内存的权限,r读,w写,x,s共享,p私有; 偏移量:库在进程里地址范…
两成开发者月薪超 1.7 万、算法工程师最紧缺! | 中国开发者年度报告
整理 | 郭芮 责编 | 唐小引 出品 | CSDN(ID:CSDNnews) “求知若饥,虚心若愚”——这个原本出自《全球概览》的俳句,因为乔布斯在斯坦福大学毕业演讲中的引用而备受推崇,流传成为 IT 界的至理名言之一。在…

怎么处理404 错误页面 、处理404页面、asp.net 处理404页面
说明 On 指定启用自定义错误。如果未指定 defaultRedirect,用户将看到一般性错误。 Off 指定禁用自定义错误。这允许显示标准的详细错误。 RemoteOnly 指定仅向远程客户端显示自定义错误并且向本地主机显示 ASP.NET 错误。这是默认值。 system.web 元素 下添加下边…

转载:python原生态的输入窗口抖动+输入特效
python原生态的输入窗口抖动输入特效 出处:https://coding.net/u/acee/p/PythonPowerInput/git/blob/master/test_power_input.py __author__ Administrator import sys from lib.qm_app import App from PyQt4.QtGui import * from PyQt4.QtCore import * import …
华为提出基于进化算法和权值共享的神经网络结构搜索,CIFAR-10上仅需单卡半天 | CVPR 2020...
作者 | VincentLee来源 | 晓飞的算法工程笔记导读:为了优化进化算法在神经网络结构搜索时候选网络训练过长的问题,参考ENAS和NSGA-III,论文提出连续进化结构搜索方法(continuous evolution architecture search, CARS),最大化利用…

在.Net Micro Framework中显示汉字
摘要:MF平台支持的字体是专有格式,扩展名为tinyfnt,需要用专门的转化工具才能把windows平台上的字体转换为tinyfnt字体。在.Net Micro Framework SDK中提供了一个叫做TFConvert.exe的工具,我们可以用它在命令行下将PC机上的TrueType或者OpenT…

汇编语言使用C库函数和Linux动态链接
使用printf 代码 #cpuid2.s -- Using C labrary calls .section .data output: .asciz "The processor Vender is %s\n".section .bss .lcomm buffer, 12 .section .text .globl _start _start: movl $0, %eax cpuid …

springJDBC实现查询方法二
无废话,看代码: Overridepublic List<Sites> queryAllSites(Pager pager) {String sql "select * from sakai_site order by SITE_ID limit ?,?";Object[] obj new Object[]{pager.getStart(),pager.getLimit()};List<Sites> …

全球计算机视觉顶会CVPR 2020论文出炉:腾讯优图17篇论文入选
全球计算机视觉顶级会议CVPR2020 (IEEE Conference on Computer Vision and Pattern Recognition,即IEEE国际计算机视觉与模式识别会议) 即将于2020年6月14日-19日在美国西雅图召开。本届大会总共录取来自全球论文1470篇,腾讯优图实验室入选17篇。 作为…