写点什么

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

用户头像
geange
关注
发布于: 2021 年 04 月 30 日
golang单元测试踩坑系列(一)

迫切的业务需求

在业务开发的过程中,老旧代码变成了新功能开发和测试的最大阻碍,人员变动以及。基于历史代码的功能修改可能会带来副作用,进而影响用户体验(绩效)。

面临困难

  • 老代码对接口的使用非常少

  • 业务逻辑中存在大量网络调用

改造

熟悉业务

熟悉业务是需要找到对外部依赖最少的代码,从依赖最少的代码入手可以极大减少初期编写单元测试的工作量。外部依赖代码主要是对数据库的操作、接口调用等等。将对外的依赖抽离出来,是最简单的一种处理方式。

逐步改进

将网络调用通过接口形式调用


假设有一个场景,需要通过接口获取一个用户的信息,并判断用户的性别,代码如下(在业务场景中这样的代码非常常见):


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时可以不需要网络请求,只是简单伪装返回数据。在测试的时候我们就可以不需要依赖外部的服务来实现单元测试。


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方法,用于模拟实际的网络请求


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


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 就是为了解决这种问题而出现的。


编写接口测试


接口测试需要启动服务器,并且将请求指向接口地址。


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)}
复制代码


接口测试实际是作为验证外部依赖正确性的一种方式,不应该完全相信被调用方。

提高代码覆盖率

增加测试用例。。。


对您有帮助可以帮忙关注下公众号:


发布于: 2021 年 04 月 30 日阅读数: 26
用户头像

geange

关注

还未添加个人签名 2018.05.26 加入

还未添加个人简介

评论

发布
暂无评论
golang单元测试踩坑系列(一)