47

搞定Go单元测试(二)—— mock框架(gomock)

 4 years ago
source link: https://www.tuicool.com/articles/R3I3Qvv
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

通过阅读上一篇文章,相信你对怎么做单元测试已经有了初步的概念,可以着手对现有的项目进行改造并开展测试了。学会了走路,我们尝试跑起来,本篇主要介绍gomock测试框架,让我们的单元测试更加有效率。

表格驱动测试方法(Table Driven Tests)

当针对某方法进行单元测试的时候,通常不止写一个测试用例,我们需要测试该方法在多种入参条件下是否都能正常工作,特别是要针对边界值进行测试。通常这个时候表格驱动测试就派上用场了—— 当你发现你在写测试方法的时候用上了复制粘贴,这就说明你需要考虑使用表格驱动测试来构建你的测试方法了 。我们依旧来举个例子:

func TestTime(t *testing.T) {
    testCases := []struct {  // 设计我们的测试用例
        gmt  string
        loc  string
        want string
    }{
        {"12:31", "Europe/Zuri", "13:31"},     // incorrect location name
        {"12:31", "America/New_York", "7:31"}, // should be 07:31
        {"08:08", "Australia/Sydney", "18:08"},
    }
    for _, tc := range testCases {  // 循环执行测试用例
        loc, err := time.LoadLocation(tc.loc)
        if err != nil {
            t.Fatalf("could not load location %q", tc.loc)
        }
        gmt, _ := time.Parse("15:04", tc.gmt)
        if got := gmt.In(loc).Format("15:04"); got != tc.want {
            t.Errorf("In(%s, %s) = %s; want %s", tc.gmt, tc.loc, got, tc.want)
        }
    }
}
复制代码

表格驱动测试方法让我们的测试方法更加清晰和简练,减少了复制粘贴,并大大提高的测试代码的可读性。

还记得上文说单元测试也是需要维护的吗?单元测试也是代码的一部分,也应当被认真对待。记得要用表格驱动测试的方法来组织你的测试用例,同时别忘了像正式代码那样,写上相应的注释。 更多参考: github.com/golang/go/w… blog.golang.org/subtests

使用测试框架——gomock

What is gomock?

gomock是Google开源的golang测试框架。或者引用官方的话来说:“GoMock is a mocking framework for the Go programming language”。

github.com/golang/mock

Why gomock?

上篇文章末尾介绍了mock和stub相结合的测试方法,可以感受到mock与stub结合起来功能固然强大——调用顺序检测,调用次数检测,动态控制函数的返回值等等,但同时,其带来的维护成本和复杂度缺是不可忽视的,手动维护这样一套测试代码那将是一场灾难。我们期望能用一套框架或者工具,在提供强大的测试功能的同时帮我们维护复杂的mock代码。

How does it work?

gomock通过 mockgen 命令生成包含mock对象的 .go 文件,其生成的mock对象具备mock+stub的强大功能,并将我们从写mock对象中解放了出来:

mockgen -destination foo_mock.go -source foo.go -package foo //mock foo.go里面所有的接口,将mock结果保存到foo_mock.go
复制代码

gomock让我们既能使用mock与stub结合的强大功能,又不需要手动维护这些mock对象,岂不美哉?

举个栗子

在这里我们对gomock的基本功能做一个简单演示: 假设我们的接口定义在 user.go

// user.go
package user

// User 表示一个用户
type User struct {
   Name string
}
// UserRepository 用户仓库
type UserRepository interface {
   // 根据用户id查询得到一个用户或是错误信息
   FindOne(id int) (User,error)
}
复制代码

通过mockgen在同目录下生成mock文件user_mock.go

mockgen -source user.go -destination user_mock.go -package user
复制代码

然后在该目录下新建 user_test.go 来写我们的测试函数,上述步骤完成之后,我们的目录结构如下:

└── user
    ├── user.go
    ├── user_mock.go
    └── user_test.go 
复制代码

设置函数的返回值

// 静态设置返回值
func TestReturn(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	repo := NewMockUserRepository(ctrl)
	// 期望FindOne(1)返回张三用户
	repo.EXPECT().FindOne(1).Return(&User{Name: "张三"}, nil)
	// 期望FindOne(2)返回李四用户
	repo.EXPECT().FindOne(2).Return(&User{Name: "李四"}, nil)
	// 期望给FindOne(3)返回找不到用户的错误
	repo.EXPECT().FindOne(3).Return(nil, errors.New("user not found"))
	// 验证一下结果
	log.Println(repo.FindOne(1)) // 这是张三
	log.Println(repo.FindOne(2)) // 这是李四
	log.Println(repo.FindOne(3)) // user not found
	log.Println(repo.FindOne(4)) //没有设置4的返回值,却执行了调用,测试不通过
}
// 动态设置返回值
func TestReturnDynamic(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()
	repo := NewMockUserRepository(ctrl)
	// 常用方法之一:DoAndReturn(),动态设置返回值 
	repo.EXPECT().FindOne(gomock.Any()).DoAndReturn(func(i int) (*User,error) {
		if i == 0 {
			return nil, errors.New("user not found")
		}
		if i < 100 {
			return &User{
				Name:"小于100",
			}, nil
		} else {
			return &User{
				Name:"大于等于100",
			}, nil
		}
	})
	log.Println(repo.FindOne(120))
	//log.Println(repo.FindOne(66))
	//log.Println(repo.FindOne(0))
}
复制代码

调用次数检测

func TestTimes(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	repo := NewMockUserRepository(ctrl)
	// 默认期望调用一次
	repo.EXPECT().FindOne(1).Return(&User{Name: "张三"}, nil)
	// 期望调用2次
	repo.EXPECT().FindOne(2).Return(&User{Name: "李四"}, nil).Times(2)
	// 调用多少次可以,包括0次
	repo.EXPECT().FindOne(3).Return(nil, errors.New("user not found")).AnyTimes()

	// 验证一下结果
	log.Println(repo.FindOne(1)) // 这是张三
	log.Println(repo.FindOne(2)) // 这是李四
	log.Println(repo.FindOne(2)) // FindOne(2) 需调用两次,注释本行代码将导致测试不通过
	log.Println(repo.FindOne(3)) // user not found, 不限调用次数,注释掉本行也能通过测试
}
复制代码

调用顺序检测

func TestOrder(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()
	repo := NewMockUserRepository(ctrl)
	o1 := repo.EXPECT().FindOne(1).Return(&User{Name: "张三"}, nil)
	o2 := repo.EXPECT().FindOne(2).Return(&User{Name: "李四"}, nil)
	o3 := repo.EXPECT().FindOne(3).Return(nil, errors.New("user not found"))
	gomock.InOrder(o1, o2, o3) //设置调用顺序
	// 按顺序调用,验证一下结果
	log.Println(repo.FindOne(1)) // 这是张三
	log.Println(repo.FindOne(2)) // 这是李四
	log.Println(repo.FindOne(3)) // user not found
	
	// 如果我们调整了调用顺序,将导致测试不通过:
	// log.Println(repo.FindOne(2)) // 这是李四
	// log.Println(repo.FindOne(1)) // 这是张三
	// log.Println(repo.FindOne(3)) // user not found
}
复制代码

上面的示例只展现了 gomock 功能的冰山一角,在本篇中不再深入讨论,更多用法请参考文档。

更多官方示例: github.com/golang/mock…

如果你完成了上一章的小练习,尝试动手使用gomock改造一下吧!

总结一下

本篇介绍了表格驱动测试与gomock测试框架。运用表格驱动测试方法不仅能使测试代码更精简易读,还能提高我们测试用例的编写能力,无形中提升了单元测试的质量。gomock的功能十分丰富,想掌握各种骚操作还是要细心阅读一下官方示例,但通常20%的常规功能也足够覆盖80%的测试场景了。

表格驱动单元测试和gomock将我们的单元测试效率与质量提升了一个档次。在下一篇文章中,将介绍 testify 断言库,继续优化我们的单元测试。


Recommend

  • 6
    • knasmueller.net 3 years ago
    • Cache

    Go: How to Mock Repositories with GoMock

    Go Go: How to Mock Repositories with GoMock Share on Creating mock objects for data repositories is a common requirement in web application develop...

  • 31
    • studygolang.com 5 years ago
    • Cache

    使用 Gomock 进行单元测试

    使用 Gomock 进行单元测试 原文地址: 使用 Gomock 进行...

  • 4

    golang进阶:怎么使用Gomock进行单元测试 评分: 4.5 作者: Ryan Lu 类别: golang

  • 7
    • jiajunhuang.com 2 years ago
    • Cache

    使用 gomock 测试 Go 代码

    使用 gomock 测试 Go 代码 gomock 是 Google 推出的用于 Go 的 mock 工具。它的大致用法是: 需要 mock 的地方,使用接口 执行 mockgen 生成代码 导入生成的代码,并且开始设置 mock 函数的行为 我们首...

  • 5

    6.3 利用Go语言接口进行Mock单元测试 2022-11-23  约 1117 字   预计阅读 3 分钟    次阅读  单元测试重点是对代码逻辑进行测试,也就是证明:为什么你的代码是正确的。Mock测试是单元测试中常用的一种手段,特别是对于...

  • 0

    为什么使用Mock进行单元测试?从功能开发完成的定义来看,至少包括:代码本身、文档及单元测试。而往往在实际开发中,由于需求的不停的变化,导致文档及单元测试是开发过程中直接被忽略的内容。反观优秀的开源项目,在全球...

  • 3

    python3的单元测试模块mock与性能测试模块cProfile首页 - Python/2019-06-14

  • 7

    Golang高效编写单元测试的技巧之Mock codingcn · 大约12小时之前 · 187...

  • 20

    在上一篇,介绍了表格驱动测试方法和gomock测试框架,大大提升了测试效率与质量。本篇将介绍在测试中引入断言(assertion),进一步提升测试效率与质量。 为什么需要断言库 我们先来看看Go标准包中为什么没有断言,官...

  • 4
    • www.justdojava.com 2 years ago
    • Cache

    Mockito 一个优秀的 Mock 测试框架

    Hello 大家好,我是阿粉,日常工作中很多时候我们都需要同事间的相互配合协作完成某些功能,所以我们经常会遇到服务或者应用内不同模块之间要互相依赖的场景。比如下面的场景,serviceA 中的 methodA() 方式依赖 serviceB

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK