聊聊单元测试

背景

关于单元测试,其实是我们讨论的非常多的一点,作为一个测试人员,笔者唯一没怎么接触的测试,其实就是单元测试。这段时间刚好在开发一些平台,在代码中也涉及到了这块,因此记录一下自己的一些想法。

笔者用一个场景来说明一下思路。

开发一个查询接口,接受页面传入的参数,再查询配置服务获取数据库的配置信息。最后拼成SQL之后查询结果返回。

一个常见的代码

笔者这里用Go写一个伪代码来演示,忽略那些特有的语法,相信单纯看逻辑应该是没问题的。

func GetSomething(c *Request) {
    userName := c.Query("user_name")
    page := c.Query("page")
    size := c.Query("size")

    if userName == ""{
        return error
    }
    if page == 0{
        return error
    }
    if size == 0{
        return error
    }

    rsp, err := QryDbInfoFromConfigCenter()
    if err != nil{
        return err
    }

    database := rsp.Get("Database")
    table := rsp.Get("Table")
    if database == ""{
        return error
    }
    if table == ""{
        return error
    }

    sql := fmt.Sprintf("select * from %s.%s where username='%s' and offset %d limit %d", database, table, userName, page, size)

    resp, err := DoQryInfo(tableAddr, sql)
    if err != nil{
        return err
    }

    if resp.Code != 0{
        return error
    }
    for(i:=0;i<len(resp.Data);i++){
        if resp.Data[i].status == 0{
            resp.Data[i].nickStatus = "成功"
        } else if resp.Data[i].status == 1{
            resp.Data[i].nickStatus = "失败"
        }
    }
    return resp.Data
}

上面的代码是一个非常典型的写法,这种线性的写法几乎存在于接触的80%的代码中。毫无疑问它是能够正常工作的,并且书写也非常方便,整个流程符合正常的线性思维。

但是,如果要对这样的代码去做单元测试,几乎没办法进行单元测试。因为它的每个步骤都耦合在一起,如果要测试,就必须准备一个查询db配置的服务,准备一个有数据的db。这样做单元测试的成本确实是非常高,测试用例于环境强关联,局限性非常大,并且跟做集成测试几乎没有区别。

笔者眼中的单元测试

笔者眼中的单元测试应该有这么几点:

  1. 不跟任何环境绑定,任意一个环境都能执行
  2. 要能够覆盖代码中所有于外部调用之外的代码
  3. 外部依赖不使用Mock或者部署真实服务来处理,而是放弃,留给集成测试。

改动原则

这里其实涉及到了代码的变动,争议应该是非常大的,笔者这里阐述自己的理解。

这个查询功能大致是这样:接收请求数据->检查数据是否合法->查询DB信息->检查返回信息的合法->对数据做一定的转换(生成SQL)->请求DB查询->解析返回结果->返回结果做一定的处理->返回。

大致可以分成这10步,其中除去开头的接受数据和返回结果,有8步。其中外部依赖的是2步,查询db信息请求db查询。其他的步骤都是一些数据的转换和处理。那么代码应该把这些抽离出来作为单一功能的方法,这样单纯的数据处理的方法,就能够不依赖任何环境从而进行单元测试验证。

从这个思路来推导

  1. 请求数据合法性检查没问题,就可以保证没有非法的参数进到流程中。
  2. 查询配置中心返回的数据是合法的,就可以保证拼出来的SQL是正确的。
  3. 生成SQL的逻辑没有问题,就可以保证请求db查询的数据没有问题。
  4. 查询回来的数据结构转换没有问题,那么返回的数据就不会有问题。

总结一下,就是通过这样的拆分,确保了我们请求外部服务的时候,参数一定是按照约定传的,如果有改动破坏了这个约定,单元测试就能发现。同样,返回数据的解析处理也是按照约定处理的,如果有改动破坏了这个约定,单元测试也是能够发现的。

如果有了这样的保证,那么外部服务是否真的去请求,实际上区别并不是特别大。

改动代码的结构

同样用Go的伪代码来写这个改造后的代码。

type Rqst struct{
    UserName string 
    Page int32 
    Size int32 
}

type DatabaseInfo struct {
    Database string
    Table string
}

func NewRqst(c *Request) *Rqst{
    var r = new(Rqst)
    r.UserName := c.Query("user_name")
    r.Page := c.Query("page")
    r.Size := c.Query("size")
}

func(r Rqst)checkParam()error{
    if r.UserName == ""{
        return error
    }
    if r.Page == 0{
        return error
    }
    if r.Size == 0{
        return error
    }
}

func (r Rqst)buildQrySQL(d *DatabaseInfo)string{
    sql := fmt.Sprintf("select * from %s.%s where username='%s' and offset %d limit %d", d.Database, d.Table, r.UserName, r.Page, r.Size)
}

func NewQryRsp(rsp *QryResp) *DatabaseInfo{
    var d = new(DatabaseInfo)
    d.Database := rsp.Get("Database")
    d.Table := rsp.Get("Table")
    return d
}

func(d *DatabaseInfo)checkResp(){
    if d.Database == ""{
        return error
    }
    if d.Table == ""{
        return error
    }
    return d
}

func checkQryDbResult(resp *DoQryInfoResp)error{
    if resp.Code != 0{
        return error
    }
    return nil
}

func setNickStatus(resp *DoQryInfoResp){
    for(i:=0;i<len(resp.Data);i++){
        if resp.Data[i].status == 0{
            resp.Data[i].nickStatus = "成功"
        } else if resp.Data[i].status == 1{
            resp.Data[i].nickStatus = "失败"
        }
    }
}

func GetSomething(c *gin.Context) {
    r := NewRqst(c)
    err := r.checkParam()
    if err != nil{
        return err
    }
    _rsp, err := QryDbInfoFromConfigCenter()
    if err != nil{
        return err
    }
    rsp := NewQryRsp(_rsp)
    err = rsp.checkResp()
    if err != nil{
        return err
    }
    sql := r.buildQrySQL(rsp)
    resp, err := DoQryInfo(tableAddr, sql)
    err = checkQryDbResult(resp)
    if err != nil {
        return err
    }
    setNickStatus(resp)
    return resp.Data
}

从改造的结果来看,代码变多了很多,主要就是更多的结构体的定义和方法声明的代码,实际的业务代码来看是差不多的。

但是改造后的优点确非常的明显,主流程GetSomething中的代码更清晰简单,阅读的人可以很快的明白这个接口到底只做什么的,而不需要完全读懂这个代码。

同样,改造后,每个方法都是可以单独的写对应的测试用例,而这样写出来的单元测试用例由于只是一些数据变动的处理逻辑,没有涉及到外部的请求,因此是可以在任意环境执行,并且结果可靠有效,不会出现环境问题导致的用例失败。

总结和一些思考

这样的做法,实际上涉及到了代码结构的变动,个人认为这样写会更加优雅易读,但是由于每个人的想法、思维方式等都不同,包括工作经历,也会影响这些,因此关于代码优雅性这块不做更多的讨论,读者可以保留自己的想法。

对于单元测试来说,笔者做过一部分代码改造来实践这部分内容,发现效果还不错,确确实实帮忙发现了一些问题,应该说,是具有一定的合理性的。

最后还有一点关于代码覆盖率的,业界普遍的要求单元测试的覆盖率是80%。按照笔者最近的实践来看,如果你的代码没有写很多废话或者废逻辑,这么干要达到80%,对于业务不复杂(也就是数据处理部分少)的工程来说还是有难度的。这或许是另一个值得探讨和学习的点。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容

  • 我是一个着迷于产品和运营的技术人,乐于跨界的终身学习者。欢迎关注我的个人公众号「跨界架构师」每周五11:45 按时...
    跨界架构师阅读 237评论 0 1
  • 遇到问题多思考、多查阅、多验证,方能有所得,再勤快点乐于分享,才能写出好文章。 一、单元测试 1. 定义与特点 单...
    程序熊大阅读 8,211评论 7 62
  • 作为一名质量管理人员,从刚入行时就接触到单元测试:需求提测时要保证一定的单元测试覆盖率作为提测准入;进行线上问题c...
    Rechel_uniq阅读 699评论 0 1
  • 本篇主要是聊一聊以下几个方面的内容: 为什么要单元测试 单元测试框架 单元测试的好处 单元测试与重构 1. 为什么...
    塞外的风阅读 240评论 0 3
  • 一、教程目标 学会基于AssertJ的断言技术; 学会基于AssertJ-DB的数据库断言技术; 学会基于JMoc...
    文景大大阅读 1,345评论 1 0