目前团队并没有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})

这段代码大致意思是:

  1. 从AppLinks表中检索出所有有效数据(第5行)
  2. 遍历1中结果,查询每个信息对应的email(第11,12行)
  3. 将1中渠道的link信息和2中渠道的email信息组装成一条记录(第14,15行)

这段代码有好几个问题:

  1. 如果异常发生在第7行之前,执行到第19行时由于data_list未声明而被使用,将抛出异常
  2. 两处查询数据库可能产生的异常很不方便测试
  3. 第8行判断没有必要,而且造成一层嵌套。如果返回的数组,则可以进入异常处理;如果返回空数组,第21行也能正确处理。
  4. 第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函数职责也变得清晰。

分析这段代码,我们可以列出大致的测试点:

  1. _get_all_valid_applinks/_get_email_by_user_id抛出异常

  2. _get_all_valid_applinks/_get_email_by_user_id返回None

  3. _get_all_valid_applinks返回空List

  4. _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,从而我们可以干涉其内部执行。这也是一种非常常用的设计。

谈一次单元测试驱动代码重构相关推荐

  1. .NET重构—单元测试的代码重构

    阅读目录: 1.开篇介绍 2.单元测试.测试用例代码重复问题(大量使用重复的Mock对象及测试数据) 2.1.单元测试的继承体系(利用超类来减少Mock对象的使用) 2.1.1.公用的MOCK对象: ...

  2. 代码重构:面向单元测试

    作者:杜沁园(悬衡) 重构代码时,我们常常纠结于这样的问题: 需要进一步抽象吗?会不会导致过度设计? 如果需要进一步抽象的话,如何进行抽象呢?有什么通用的步骤或者法则吗? 单元测试是我们常用的验证代码 ...

  3. 从头到脚说单测——谈有效的单元测试

    导语 非常幸运的是,从4月份至今,我能够全身心投入到腾讯新闻的单元测试专项任务中,从无知懵懂,到不断深入理解的过程,与开发同学互帮互助,受益匪浅.在此过程中,得到了质量总监.新闻总监和乔帮主的倾囊指导 ...

  4. 从头到脚说单测——谈有效的单元测试(下篇)

    导读 在<从头到脚说单测--谈有效的单元测试(上篇)>中主要介绍了:金字塔模型.为何要做单测.单测的阶段及指标,在下篇中我们主要介绍关于mock.和如何不要滥用mock.用例编写的策略等更 ...

  5. 把三千行代码重构为15行

    2019独角兽企业重金招聘Python工程师标准>>> 如果你认为这是一个标题党,那么我真诚的恳请你耐心的把文章的第一部分读完,然后再下结论.如果你认为能够戳中您的G点,那么请随手点 ...

  6. 工程师必知的代码重构指南

    作者 | CATE LAWRENCE 译者 | 冬雨 策划 | 蔡芳芳 本指南将带你了解进行代码重构的好处.可能遇到的挑战.可以采用的工具和最佳实践,以及重构和技术债务之间的区别. 我们都在寻找清理代 ...

  7. 代码重构的方法和经验_关于烂代码优化重构的几点经验

    是否已经读过前面两篇关于烂代码和好代码的文章? 这些让人抓狂的烂代码,你碰到几种? 什么才是好代码.高质量代码? 工作中,总会不可避免的接触到烂代码,就像之前说的,几乎没有程序员可以完全避免写出烂代码 ...

  8. 代码重构技巧宝典,学透本篇就足够了!

    本文来源:http://n5d.net/ma76k 关于重构 为什么要重构 1_代码重构漫画.jpeg 项目在不断演进过程中,代码不停地在堆砌.如果没有人为代码的质量负责,代码总是会往越来越混乱的方向 ...

  9. 系统重构的原则代码重构的原则

    作者:[美]马丁•福勒(Martin Fowler) 译者:熊节, 林从羽 前一章所举的例子应该已经让你对重构有了一个良好的感觉.现在,我们应该回头看看重构的一些大原则. ##2.1 何谓重构 一线的 ...

最新文章

  1. ThinkPHP 模板循环输出 Volist 标签
  2. 服务器硬盘SAS与SATA区别介绍
  3. Super BOM应用步骤总结
  4. [英]Promises Don't Come Easy
  5. Edge浏览器网页怎么收藏 Edge浏览器网页收藏图文教程
  6. 开源维护者,必有一战!
  7. Auto-ML之自动化特征工程
  8. python图像转字符画_Python3:图片转字符画
  9. 【Keras】使用数据生成器(data generators)解决训练数据内存问题
  10. 基于CarMaker的C-NCAP主动安全系统试验仿真(四)
  11. miRNA数据库篇——mirBase(序列数据库)
  12. DSP入门小白学习日记第二篇
  13. 图片复印如何去除黑底_如何将扫描后的图片底色去掉
  14. python分号报错_go、java已经python中分号的使用
  15. 【BZOJ2959】长跑(LCT,双连通分量,并查集)
  16. C++_输入一个字符串,并逆序输出
  17. 牛津3000词汇表(The Oxford 3000™)
  18. 安装黑群晖,打开群晖助手初始化失败问题,报错35(ESXI6.7虚拟机安装黑群辉最新版DSM6.2.1)
  19. WinSCP连接不上虚拟机
  20. Au 入门系列之七:应用效果器

热门文章

  1. python deque双端队列的神奇用法
  2. Python Qt GUI设计:将UI文件转换为Python文件的三种妙招(基础篇—2)
  3. GitHub开源:一键生成前后端代码神器
  4. 关于人脸识别数据库的几点介绍
  5. Linux Wi-Fi 编程API介绍
  6. python一个月能掌握吗_零基础python入门分析,如何做到一个月学会(深思极恐)...
  7. c语言arr什么意思6,初识C语言(六)
  8. java 读取excel_Java12POI操作Excel
  9. Udacity机器人软件工程师课程笔记(二十八) - 卷积神经网络实例 - Fashion-MNIST数据集
  10. 2021-08-05 Ubuntu18.04安装ROS出现的一些问题