关于java:从头到脚说单测谈有效的单元测试上篇

3次阅读

共计 6184 个字符,预计需要花费 16 分钟才能阅读完成。

一. 为单元测试“正名”
我已经认为,单元测试面向的是一个函数。任何走出一个函数的测试,都不是单元测试。
其实,对“单元”的定义取决于本人。如果你正在应用函数式编程,一个单元最有可能指的是一个函数。你的单元测试将应用不同的参数调用这个函数,并断言它返回了期待的后果;在面向对象语言里,下至一个办法,上至一个类都能够是一个单元(从一个繁多的办法到一整个的类都能够是一个单元)。用意很重要(“用意”二字是本文中第一次提到,它很重要)
咱们有单元测试、增量测试、集成测试、回归测试、冒烟测试等等,名字十分多。谷歌看到这种“百家争鸣”的景象,创建了本人的命名形式,只分为小型测试、中型测试和大型测试。
·小型测试,针对单个函数的测试,关注其外部逻辑,mock 所有须要的服务。
小型测试带来优良的代码品质、良好的异样解决、优雅的错误报告
·中型测试,验证两个或多个制订的模块利用之间的交互
·大型测试,也被称为“零碎测试”或“端到端测试”。大型测试在一个较高层次上运行,验证零碎作为一个整体是如何工作的。

论断:咱们的单元测试,既能够针对一个函数写 case,也能够依照函数的调用关系串起来写 case。
二. 金字塔模型
在金字塔模型之前,风行的是冰淇淋模型。蕴含了大量的手工测试、端到端的自动化测试及大量的单元测试。造成的结果是,随着产品壮大,手工回归测试工夫越来越长,品质很难把控;自动化 case 频频失败,每一个失败对应着一个长长的函数调用,到底哪里出了问题?单元测试少的可怜,根本没作用。

Mike Cohn 在他的着作《Succeeding with Agile》一书中提出了“测试金字塔”这个概念。这个比喻十分形象,它让你一眼就晓得测试是须要分层的。它还通知你每一层须要写多少测试。
测试金字塔自身是一条很好的教训法令,咱们最好记住 Cohn 在金字塔模型中提到的两件事:
·编写不同粒度的测试
·档次越高,你写的测试应该越少

同时,咱们对金字塔的了解绝不能止步于此,要进一步了解:

我把金字塔模型了解为——冰激凌消融了。就是指,最顶部的“手工测试”实践上全副要自动化,向下消融,优先全副思考消融成单元测试,单元测试笼罩不了的 放在中间层(分层测试),再笼罩不了的才会放到 UI 层。因而,UI 层的 case,能没有就不要有,跑的慢还不稳固。依照乔帮主的说法,我不分单元测试还是分层测试,对立都叫自动化测试,那就应该把所有的自动化 case 看做一个整体,case 不要冗余,单元测试能笼罩,就要把这个 case 从分层或 ui 中去掉。
越是底层的测试,牵扯到相干内容越少,而高层测试则涉及面更广。比方单元测试,它的关注点只有一个单元,而没有其它任何货色。所以,只有一个单元写好了,测试就是能够通过的;而集成测试则要把好几个单元组装到一起能力测试,测试通过的前提条件是,所有这些单元都写好了,这个周期就显著比单元测试要长;零碎测试则要把整个零碎的各个模块都连在一起,各种数据都筹备好,才可能通过。
另外,因为波及到的模块过多,任何一个模块做了调整,都有可能毁坏高层测试,所以,高层测试通常是绝对比拟软弱的,在理论的工作中,有些高层测试会牵扯到内部零碎,这样一来,复杂度又在一直地晋升。
三. 为什么做单测
这个问题咱们躲避不掉。新闻是这次研发模式改革的主力军之一,所以自上而下的推动让这个问题不那么辣手:做了就是做了。不做,却又有那么多的理由:
(收集到的吐槽实在声音)
· 单元测试节约了太多的工夫
· 单元测试仅仅是证实这些代码做了什么
· 我是很棒的程序员,我是不是能够不进行单元测试?
· 前面的集成测试将会抓住所有的 bug
· 单元测试的老本效率不高我把测试都写了,那么测试人员做什么呢?
· 公司请我来是写代码,而不是写测试
· 测试代码的正确性,并不是我的工作
单元测试的意义
·单元测试对咱们的产品质量是十分重要的。
·单元测试是所有测试中最底层的一类测试,是第一个环节,也是最重要的一个环节,是惟一一次有保障可能代码覆盖率达到 100% 的测试,是整个软件测试过程的根底和前提,单元测试避免了开发的前期因 bug 过多而失控,单元测试的性价比是最好的。
·据统计,大概有 80% 的谬误是在软件设计阶段引入的,并且修改一个软件谬误所需的费用将随着软件生命期的停顿而回升。谬误发现的越晚,修复它的费用就越高,而且呈指数增长的趋势。作为编码人员,也是单元测试的次要执行者,是惟一可能做到生产出无缺点程序这一点的人,其余任何人都无奈做到这一点
·代码标准、优化,可测试性的代码
·释怀重构
·自动化执行 three-thousand times
上面这张图,来自微软的统计数据:bug 在单元测试阶段被发现,均匀耗时 3.25 小时,如果漏到零碎测试阶段,要花费 11.5 小时。

上面这张图,旨在阐明两个问题:85% 的缺点都在代码设计阶段产生,而发现 bug 的阶段越靠后,消耗老本就越高,指数级别的增高。所以,在晚期的单元测试就能发现 bug,省时省力,一劳永逸,何乐而不为呢。

单元测试特地耗时?
不能一刀切,不能只盯着单测阶段的耗时。
我采访了新闻客户端、后盾的开发,首先必定的是,单测会减少开发量、减少开发时长;

在《单元测试的艺术》这本书提到一个案例:找了开发能力相近的两个团队,同时开发相近的需要。进行单测的团队在编码阶段时长增长了一倍,从 7 天到 14 天,然而,这个团队在集成测试阶段的体现十分顺畅,bug 量小,定位 bug 迅速等。最终的成果,整体交付工夫和缺点数,均是单测团队起码。

单测,存在即正当。一方面,须要把单测放在整个迭代周期来观测其成果;一方面,写单测也是技术活,写得好的同学,工夫少代码品质高(也即,不是说写了单测,就能写好单测)
谁来写单测呢?
·开发同学写单测
·测试同学具备写单测的能力。重点在于开发脚手架、分层测试 / 端到端测试
增量还是存量
·单测 case 针对增量代码
·当存量代码呈现大规模重构,后者品质暴露出极大危险时,都是推动补全单测的好时机
四. 单元测试的阶段
一. 狭义的单元测试,咱们指这三局部的有机组合:
·code review
·动态代码扫描
·单元测试用例编写
二. 联合新闻的实际,我把单测成长的过程分为 4 个指标,别离为:
·会写,全员可写
·写的好,同时关注可测性问题,试点解决
·辨认可测性问题,纯熟应用重构办法进行重构;辨认代码架构设计问题;case 与业务代码同步编写
·TDD。但这个指标是冀望,不能作为必须实现的指标。

截至发稿当天,新闻处于第三阶段,即,每个迭代均能产出高质量的 case,人数笼罩和需要笼罩均较高;关注重点在于可测性,时刻重视重构。

五. 单元测试的指标
还挺难堪的,不太有间接的指标去掂量单测的成果。咱们也常常被问到,“怎么证实你们新闻单测的作用呀?”
·bug 类指标(间接指标):间断迭代的 bug 总数趋势、迭代内新建 bug 的趋势、千行 bug 率
·单测的需要覆盖度(50% 以上),参加人员覆盖度(80% 以上)
·单测 case 总数趋势,代码行增量趋势
·增量代码的行覆盖率(接入层 80%,客户端 30%)
·单函数圈复杂度(低于 40),单函数代码行数(低于 80),扫描告警数
在迭代需要继续高吞吐量的前提下,以新闻 iOS 的数据为例:




六. go 单元测试框架选型
根本选型:testify + gomonkey
附加:httptest + sqlmock


前提
·测试文件,以_test.go 结尾,与被测文件放于雷同目录
·测试函数,函数名以 Test 结尾,并且随后的第一个字符必须为大写字母或下划线,如:TestParseReq_CorrectNum_TableDriven
·测试函数,参数为 t testing.T;对于 bench 测试,参数为 b testing.B
·运行命令行,我的文章有深刻解说:go test 命令行

testify 惯例用法
https://github.com/stretchr/t…
testify 基于 gotesting 编写,所以语法上、执行命令行与 go test 齐全兼容
反对大量高效的 api,比方:
assert.Equal:惯例比照,是把两者别离换成[]byte 去严格比对
assert.Nil:判断对象为 nil 时,有时对 err 判空时也用
assert.Error:判断 err 的具体类型和内容
assert.JSONEq:这个比拟有用,比照 map 时;或者比照 struct 的时候,也会先转为 map,在用这个 api 去做比照,如上面这个例子,我封装了倡议的办法去将 struct 转换为 string(json):

· 反对 suite,用例集治理
· 运行时,能够指定用例集执行

· 自带 mock 工具,但只反对接口办法的 mock,而且用法绝对简单
· table-driven

gomonkey 用法(蓝色字体示意罕用)
https://github.com/agiledrago…
https://studygolang.com/artic…

·反对为一个函数打一个桩
·反对为一个成员办法打一个桩
·反对为一个全局变量打一个桩
·反对为一个函数变量打一个桩
·反对为一个函数打一个特定的桩序列
·反对为一个成员办法打一个特定的桩序列
·反对为一个函数变量打一个特定的桩序列
·table-driven 的形式定义一系列 stub
留神,对内联函数的 Stub,go test 命令行肯定要加上参数才可失效。见官网文档。所以,我的命令行默认加上 -gcflags=all=- l 就行了。

我设置了一些 goland 的代码模板,放在附件中。
ApplyFunc 是对外部函数 Stub(非类办法)
/* 用法:gomonkey.ApplyFunc(被 stub 函数名, 被 stub 函数签名) 函数返回值
    * 例子:
    patches := gomonkey.ApplyFunc(fake.Exec, func(_ string, _ …string) (string, error) {
    return outputExpect, nil
                   })
 */

patches := gomonkey.ApplyFunc(lcache.GetCache, func(_ string) (interface{}, bool) {
return getCommentsResp()
})
defer patches.Reset()
ApplyMethod 是对类函数 Stub。但这里留神,要被 stub 的形式是公有办法,gomonkey 通过反射是找不到的,有两种解决办法:1)应用增强版的 gomonkey;2)不 Stub 它,而是抉择走进这个函数,这个话题在前面专题谈 mock 的时候说。
/* 用法:gomonkey.ApplyMethod(反射类名, 被 stub 函数签名) 函数返回值
    * 例子:
    var s *fake.Slice
    patches := ApplyMethod(reflect.TypeOf(s), “Add”, func(_ *fake.Slice, _ int) error {
                return nil
            })
 */
 
var ac *auth.AuthCheck
patches := gomonkey.ApplyMethod(reflect.TypeOf(ac), “PrepareWithHttp”, func(_ auth.AuthCheck, http.Request, …auth.AuthOption) error {
return fmt.Errorf(“prepare with nil object”)
})
defer patches.Reset()
ApplyMethodSeq 是对同一个 Stub 的函数返回不同的后果
/* 用法:gomonkey.ApplyMethodSeq(类的反射,” 被 stub 函数名 ”, 返回构造体);
    Params{info1}, 中括号内为被 stub 函数的返回值列表;
    Times 为失效次数
    * 例子:
    e := &fake.Etcd{}
    info1 := “hello cpp”
    info2 := “hello golang”
    info3 := “hello gomonkey”
    outputs := []OutputCell{
         {Values: Params{info1, nil}},
         {Values: Params{info2, nil}},
         {Values: Params{info3, nil}},
      }
      patches := ApplyMethodSeq(reflect.TypeOf(e), “Retrieve”, outputs)
      defer patches.Reset()
 */
conn := &redis.RedisConn{}
patch1 := gomonkey.ApplyFunc(redis.NewRedisHTTP, func(serviceName string, _ string) *redis.RedisConn {
conn := &redis.RedisConn{
redis.RedisConfig{},
&redis.RedisHelper{},
}
return conn
})
defer patch1.Reset()
 
// mock redis data. 返回空和不为空的状况
outputCell := []gomonkey.OutputCell{
{Values: gomonkey.Params{“12”, nil}, Times: 1},
{Values: gomonkey.Params{“”, nil}, Times: 1},
}

patchs := gomonkey.ApplyMethodSeq(reflect.TypeOf(conn.RedisHelper), “Get”, outputCell)
defer patchs.Reset()
 先举这几个例子,具体的能够在下面的链接文章中全面失去。
这里补充一点,对类办法进行 stub,必须要找到该办法对应的实在的类(构造体),举个例子:
// 被测函数中有如下一段,其中的 Get 办法咱们想 stub 掉,只有找到 Get 办法对应的类就好了
readCountStr, _ := conn.Get(redisKey)
if len(readCountStr) == 0 {
return 0, nil
}
定位 conn,是 RedisConn 类型的 struct
 
type RedisConn struct {
RedisConfig
*RedisHelper
}
所以第一次,我用 gomonkey.AppleyMethod 时这么写:
 
patches := gomonkey.ApplyMethod(reflect.TypeOf(RedisConn),”Get”, func(_ redis.RedisHelper,_ string, _ []string) ([]string, error){
return info,err_notNil
})
defer patches.Reset()

WeTest 小编揭示:上篇的内容就到这里,置信大家必定还没看够吧~ 在下篇会说到对于 mock、和如何不要滥用 mock 等等更多精彩的内容,让咱们连忙一起来看下吧~《从头到脚说单测——谈无效的单元测试(下篇)》

正文完
 0