谈一次单元测试驱动代码重构(代码片段)

breaksoftware breaksoftware     2023-02-23     695

关键词:

目录

监测抛出异常

监测返回None

返回空List/dict


        目前团队并没有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 = None
        
    def _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.email
    
    def _email_empty(self, user_id):
        LOG_WARNING("need to set email for user0".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_list
        
    def 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):
        pass

    def teardown_class(self):
        pass

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

代码重构:面向单元测试(代码片段)

...何进行抽象呢?有什么通用的步骤或者法则吗?单元测试是我们常用的验证代码正确性的工具,但是如果只用来验证正确性的话,那就是真是“大炮打蚊子”--大材小用,它还可 查看详情

谈一次javaweb系统的重构思路

——略谈Javaweb软件如何提供二次开发接口 接手公司的一个Javaweb软件产品,该软件采用传统的dwr框架。dwr框架相当于一个中间层,使得javascript能够识别Java类对象,进而能够调用Java类对象的方法。该软件要为项目部同... 查看详情

单元测试汇总(代码片段)

转自:https://blog.csdn.net/u012933335/rss/list[原]Android单测调研篇1.为什么做单测单测的好处减少bug快速定位bug提高代码质量减少调试时间放心重构不得不写单测的原因在成为大牛的路上,单测是必备技能单测可以给你信心保住面子难... 查看详情

单元测试(代码片段)

一、什么是单元测试单元测试并不只是为了验证你当前所写的代码是否存在问题,更为重要的是它可以很大程度的保障日后因业务变更、修复Bug或重构等引起的代码变更而导致(或新增)的风险。同时将单元测试提前到编写正式... 查看详情

卓越工程之单元测试在行权鉴权中的实践(代码片段)

...生动,让人坚持读下去就很难。2.书中强调,每做一次代码的重构,即使修改很小的部分&# 查看详情

python单元测试(代码片段)

...发而动全身",所以对于Python项目,特别是大型项目来说单元测试来保证代码质量是非常有必要的。单元测试(UnitTesting)1.针对程序模块进行正确性检验2.一个函数、一个类进行验证3.自底向上保证程序的正确性 单元测试的目的 查看详情

单元测试,代码测试代码(代码片段)

#单元测试,代码测试代码针对函数、类,检测他的某个方面是否有问题的测试开发测试用例是一组单元测试,每个单元测试是一起核实函数和类在各种情况下的行为都符合要求为什么要做单元测试?1、单元测试->集成测试->2... 查看详情

2018-2019-220175308实验二《面向对象程序设计》实验报告(代码片段)

...验二《Java面向对象程序设计》实验报告一、前期准备:单元测试和TDD:(一)单元测试我们首先要会写三种代码:伪代码产品代码测试代码Java编程中,我们首先写伪代码,它与具体的编程语言无关,从意图层面来解决问题,是... 查看详情

jest单元测试入门(代码片段)

Jest单元测试入门今天,我们要讲的是Jest单元测试的入门知识。为何要进行单元测试?在学习Jest之前,我们需要回答一个问题:为何要进行单元测试?编写单元测试可以给你带来很多好处:将测试自动化,无需每次都人工测试。... 查看详情

单元测试基础(代码片段)

为什么要有这篇文章呢?个人在不断实践中越发觉得,单元测试对于代码质量的保障真的太有意义了,至少能体现在如下两个方面:①让你写出更好的代码,可测试的代码一定是优雅的代码(为了可测试,你必须要解耦,必须要... 查看详情

驱动模块和装模块的概念——junit单元测试案例(代码片段)

...是软件产品的组成的部分。虽然各个模块开发好了,在做单元测试时,也是需要写驱动模块与桩模块的。因为做单元测试一个最重要的原则就是把被测试的单元与其他关联模块隔离开来进行测试。测试代码:PublicclassD 查看详情

单元测试运行原理探究(代码片段)

前言单元测试是软件开发过程中的重要一环,好的单测可以帮助我们更早的发现问题,为系统的稳定运行提供保障。单测还是很好的说明文档,我们往往看单测用例就能够了解到作者对类的设计意图。代码重构时也离... 查看详情

单元测试运行原理探究(代码片段)

前言单元测试是软件开发过程中的重要一环,好的单测可以帮助我们更早的发现问题,为系统的稳定运行提供保障。单测还是很好的说明文档,我们往往看单测用例就能够了解到作者对类的设计意图。代码重构时也离... 查看详情

实验二

一、单元测试和TDD用程序解决问题时,要学会写以下三种代码:伪代码产品代码测试代码正确的顺序应为:伪代码(思路)→测试代码(产品预期功能)→产品代码(实现预期功能),这种开发方法叫“测试驱动开发”(TDD)。... 查看详情

工作3年,还不会写单元测试?新技能get!(代码片段)

历史遗留代码不敢重构?每次改代码都要回归所有逻辑?提测被打回?在近期的代码重构的过程中,遇到了各式各样的问题。比如调整代码顺序导致bug,取反操作逻辑丢失,参数校验逻辑被误改等。上线前... 查看详情

为什么要做单元测试

为什么要做单元测试通常我们在做任何工作会先考虑它的回报,编写代码更是如此。如果单元测试的作用不大,没有人会愿意再写一堆无用的代码,那么单元测试到底能够给我们带来什么优点呢?如下:便于后期重构。单元测试... 查看详情

wings-让单元测试智能全自动生成(代码片段)

Wings-让单元测试智能全自动生成前言  单元测试是保证软件质量非常有效的手段,无论是从测试理论早期介入测试的理念来看或是从单元测试不受UI影响可以高速批量验证的特性,所以业界所倡导的测试驱动开发,这个里面提... 查看详情

一文带你了解单元测试和基准测试干货(代码片段)

...后再编写功能代码,每做一个修改后,都要执行一次单元测试和基准测试,以此来验证功能和性 查看详情