之前
之前对于依赖注入一直停留在文字表面理解阶段,也没有去细想为什么要这么做,开发代码的时候依然会按照习惯使用诸如构造函数之类的去构造依赖。
之前在百度的时候曾经负责过设计一个PHP框架,当时特别喜欢laravel,想把一些功能移植过来,就看过一段时间的laravel源码,当时看到源码中有很多函数只是把另一个函数包了一层,并没有太多实际的功能,一层层代码看过去很容易迷失了自己,当时还在想为什么要这么写,这次总算是体会了一把。
经过
事情的起因是这样的,我用go写了一个小功能,想基于Teambition的行云和飞流进行持续集成,其中涉及到一个步骤是单元测试,然后就遇到了第一个问题
依赖怎么搞
代码开发的时候使用了第三方的redis库,总不能每次单测,编译钱都先用go get安装一次吧,而且这样每次都要改编译脚本,这种愚蠢的做法肯定是无法接受的,得找更聪明的方法才行,于是踏上了寻找指路
go mod
之前的一些弯路在这里得提两句,目前国内的关于go的文章都太过于陈旧,就连几个go语言社区也基本没有什么有价值的了,梯子最近又被搞得不能用了,真是头疼
go mod是官方的依赖管理工具,使用也是非常简单明了,具体的使用说明我就不说了,可以直接看官方的例子,我之前看官方文档的时候一直在纳闷一件事,依赖初始化好了,然后也跟composer和npm似的依赖文件go.mod也整好了,但咋就没有composer install
或者npm install
之类的命令了,网上搜了好久也没有结果,后来发现直接不需要install,依赖会在你编译的时候自己寻找和安装,也不跟我说明白,害的我找了好久
单测需要redis怎么搞
因为使用了redis,在单测的时候肯定是没有的,而且单测环境在飞流上,也不在自己的机器上,想装个redis搞都不行
docker
首先想到的就是docker,可以使用dockerfile把基础环境搞起来,然后在docker里跑单测
然后想到一个问题,我需要两个镜像,一个是go的运行环境,一个是redis,这俩在dockerhub上都有,但是怎么把两个镜像搞到一起用又犯愁了,如果基于一个镜像,在上面装另一个系统的话可以实现目的,但总觉得这么干很蠢,一定还有更好的办法
看docker文档的时候发现了Docker Compose
通过 Compose,您可以使用 YML 文件来配置应用程序需要的所有服务。然后,使用一个命令,就可以从 YML 文件配置中创建并启动所有服务。
看着好像是我需要的东西,正当我想要搞一把试一试的时候,突然灵光乍现,想了想自己在干什么,只是为了单元测试,居然搞得这么复杂,单元测试的精髓是什么——mock,对,没错,现在才要进入本文的主题了
Gomock
想到了用mock来解决redis依赖,便开始着手找mock相关的东西,然后就发现了这个gomock,也是官方正品,用着有保障,都不需要安装,安装了go之后就自带了。然后生成mock文件,结果生成了一个大大的文件,但官方给的文档实在是太简单了,完全看不懂怎么用。
后来发现一个问题,一直都讲的是传入一个mock的对象进去,但问题是我的代码依赖压根就不是传入的,这就有点坑了,完全没法用了。然后就又去找资料看有没有别的办法,这个时候又是灵光一闪,这是不是就是传说中的依赖注入啊
依赖注入
写代码这活,其实跟修炼是一样一样的,重在一个顿悟,我就在此时突然顿悟,之前对依赖注入的理解突然加深了,然后发现当前写的这个代码并不是一个好代码,起码写起单元测试的时候无比蛋疼,接下来对我的代码进行一些改动。
func Set(id int64, token *string) error {
key := strconv.FormatInt(time.Now().Unix()+id, 10)
data := []byte(key)
has := md5.Sum(data)
*token = fmt.Sprintf("%x", has)
var client goredis.Client
if err := client.Set(*token, []byte(strconv.FormatInt(id, 10))); err != nil {
return err
}
return nil
}
func Get(token string, id *int64) error {
var client goredis.Client
val, err := client.Get(token)
if err != nil {
return err
}
*id, err = strconv.ParseInt(string(val), 10, 64)
if err != nil {
return err
}
return nil
}
原先的代码是这样的,连接redis的直接是在代码里生成的,完全没法mock,同时也发现因为要rpc调用,这俩暴露的函数接口也没法做单测
突然对以前不理解的为什么要在controller层下面再加一个service层有了新的认识,当时大家都说是为了把数据处理和逻辑处理分开,我当时觉得完全没有必要,现在才发现,这是为了方便写单测啊,当时怎么就没有人告诉我了
基于以上原因,决定把controler和具体的逻辑分开,这层就当是装饰层了吧
// Set 设置session
func Set(id int64, token *string) (err error) {
mysession := mysession.Session{&client.MyClient{}}
*token, err = mysession.Set(id)
return
}
// Get 获取session
func Get(token string, id *int64) (err error) {
mysession := mysession.Session{&client.MyClient{}}
*id, err = mysession.Get(token)
return
}
封装了一个mysession用来处理具体的逻辑
package mysession
import (
"crypto/md5"
"fmt"
"session/client"
"strconv"
"time"
)
// Session 自己封装的session
type Session struct {
Client client.Client
}
// Set 设置
func (s *Session) Set(id int64) (token string, err error) {
key := strconv.FormatInt(time.Now().Unix()+id, 10)
data := []byte(key)
has := md5.Sum(data)
token = fmt.Sprintf("%x", has)
if err := s.Client.Set(token, strconv.FormatInt(id, 10)); err != nil {
return token, err
}
return
}
// Get 获取
func (s *Session) Get(token string) (id int64, err error) {
val, err := s.Client.Get(token)
if err != nil {
return 0, err
}
id, err = strconv.ParseInt(val, 10, 64)
if err != nil {
return 0, err
}
return
}
Session的在实例化的时候就可以传入依赖的redisclient,这样在单测的时候就可以传入一个mock过后的依赖了
单测通过了,没有代码覆盖率
自作聪明将所有的测试代码放在了一个test文件夹中,所有的单测都跑了没有问题,但是一直没有代码覆盖率,我看网上也有好多人遇到了这种问题,但目前还没有人能够解答。其实解决办法是把每个包自己的测试老老实实和源代码放在一起,这样就会有代码覆盖率了
总结
这次感悟挺深的,但是开始动手写的时候却发现有太多只可意会不可言传的东西,还是写作功力不行,先救这样吧,如果阅读遇到啥问题了也可以留下评论一起讨论,我会看着把一些没写出来的东西补上的
网友评论