golang单元测试踩坑系列(一)

geange · · 2103 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

![7320c81a18c32d9c562217abdb4e0ace.jpg](https://static.studygolang.com/210430/6204f1175f278b440d7fe292f3bcc369.jpg) ## 业务需求 在业务开发的过程中,老旧代码变成了新功能开发和测试的最大阻碍,人员变动以及。基于历史代码的功能修改可能会带来副作用,进而影响用户体验(绩效)。 ## 面临困难 * 老代码对接口的使用非常少 * 业务逻辑中存在大量网络调用 ### 改造 #### 熟悉业务 熟悉业务是需要找到对外部依赖最少的代码,从依赖最少的代码入手可以极大减少**初期**编写单元测试的工作量。外部依赖代码主要是对数据库的操作、接口调用等等。将对外的依赖抽离出来,是最简单的一种处理方式。 #### 逐步改进 **将网络调用通过接口形式调用** 假设有一个场景,需要通过接口获取一个用户的信息,并判断用户的性别,代码如下(在业务场景中这样的代码非常常见): ```go type Service struct{} func (s *Service) IsBoy(userid int) bool { user, err := GetUserInfo(userid int) if err != nil { return false } return user.Sex == 1 } func GetUserInfo(userid int) (*User, error) { resp, err := resty.New().R().Get(fmt.Sprintf("http://yoururl?user=%d", userid)) if err != nil { return nil, err } var user User err = json.Unmarshal(resp.Body(), &user) return &user, err } ``` 如果需要编写单元测试,步骤如下 1. 首先需要将`GetUserInfo`抽离出来,尽量不要在代码中直接调用包含网络请求的函数。 2. 修改完成的代码如下,定义一个`ApiUser`的接口,实现该接口需要实现`GetUserInfo`方法。 3. 在`Service`中嵌入一个实现`ApiUser`的对象,方便我们后续使用mock将模拟数据注入到`Service`对象中(关于mock我们后续后有详细分析)。 4. mock简单来说就是,我们可以使用一个实现了`ApiUser`接口的类型放入到`Service`中,而这个ApiUser对象返回`*User`时可以不需要网络请求,只是简单伪装返回数据。在测试的时候我们就可以不需要依赖外部的服务来实现单元测试。 ```go type Service struct{ ApiUser } func (s *Service) IsBoy(userid int) bool { user, err := s.GetUserInfo() if err != nil { return false } return user.Sex == 1 } type ApiUser interface { GetUserInfo(userid int) (*User, error) } type apiUser struct {} func (*apiUser) GetUserInfo(userid int) (*User, error) { ... return &user, err } ``` > 把`ApiUser`嵌入到调用类型中实际上也是一种妥协的。`IsBoy(userid int)`这个方法可能在多个地方被同时调用,如果在方法的参数中传入`ApiUser`对象也是不太现实(需要注意的是,初始化`Service`对象需要记得设置`ApiUser`的值) > > 另外一种方式是使用全局变量,但个人不太推荐。 **编写逻辑测试** 定义一个`apiUserFake`类型,实现`GetUserInfo`方法,用于模拟实际的网络请求 ```go type apiUserFake struct{} func (*apiUserFake) GetUserInfo(userid int) (*User, error) { switch userid { case 0: return &User{ID: userid, Name: "G1", Age: 18, Sex: 0}, nil case 1: return &User{ID: userid, Name: "B1", Age: 19, Sex: 1}, nil default: return nil, errors.New("user_not_found") } } ``` 假设源码文件为`user.go`,在当前目录下增加一个`user_test.go` ```go package rpc import ( "github.com/stretchr/testify/assert" "testing" ) func TestService_IsBoy(t *testing.T) { assert := assert.New(t) svc := Service{ApiUser: &apiUserFake{}} assert.Equal(false, svc.IsBoy(0)) assert.Equal(true, svc.IsBoy(1)) } ``` 一个简单的逻辑测试完成。 > 这种方式写单元测试很繁琐,后面提到的mock就是为了解决这种问题而出现的。 **编写接口测试** 接口测试需要启动服务器,并且将请求指向接口地址。 ```go func TestApiUser_GetUserInfo(t *testing.T) { assert := assert.New(t) api := &apiUser{} user, err := api.GetUserInfo(1) assert.Nil(err) assert.EqualValues(&User{ID: 1, Name: "AA", Age: 23, Sex: 0}, user) } ``` > 接口测试实际是作为验证外部依赖正确性的一种方式,不应该完全相信被调用方。 #### 提高代码覆盖率 增加测试用例。 **个人公众号:** ![qrcode_for_gh_7764e4bb99e9_258.jpg](https://static.studygolang.com/210430/e0757be7ae135a46a13854521591e7c6.jpg)

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2103 次点击  ∙  1 赞  
加入收藏 微博
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传