如何写出优雅的Golang代码(六)

 go教练   2019-07-31 15:46   74 人阅读  0 条评论

我们的博客可能使用jekyll或者wordpress作为引擎,但是它们都会提供ListsPosts方法用于返回全部的文章列表,在这时我们就需要定义一个Post接口,接口要求遵循Blog的结构体必须实现ListPosts方法下面我们就一起来看看如何写出优雅的Golang代码的最后一部分介绍的

 微信截图_20190729100042.png

当我们定义好了Blog接口之后,上层Service就不再需要依赖某个具体的博客引擎实现了,只需要依赖Blog接口就可以完成对文章的批量获取功能:

package service type Service interface { ListPosts() ([]Post, error) } type service struct { blog blog.Blog } func NewService(b blog.Blog) *Service { return &service{ blog: b, } } func (s *service) ListPosts() ([]Post, error) { return s.blog.ListPosts(), nil }

如果我们想要对Service进行测试,我们就可以使用 gomock 提供的mockgen工具命令生成MockBlog结构体,使用如下所示的命令:

$mockgen -package=mblog -source=pkg/blog/blog.go > test/mocks/blog/blog.go $ cat test/mocks/blog/blog.go // Code generated by MockGen. DO NOT EDIT. // Source: blog.go // Package mblog is a generated GoMock package. ... // NewMockBlog creates a new mock instance func NewMockBlog(ctrl *gomock.Controller) *MockBlog { mock := &MockBlog{ctrl: ctrl} mock.recorder = &MockBlogMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use func (m *MockBlog) EXPECT() *MockBlogMockRecorder { return m.recorder } // ListPosts mocks base method func (m *MockBlog) ListPosts() []Post { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListPosts") ret0, _ := ret[0].([]Post) return ret0 } // ListPosts indicates an expected call of ListPosts func (mr *MockBlogMockRecorder) ListPosts() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPosts", reflect.TypeOf((*MockBlog)(nil).ListPosts)) }

这段mockgen生成的代码非常长的,所以我们只展示了其中的一部分,它的功能就是帮助我们验证任意接口的输入参数并且模拟接口的返回值;而在生成 Mock 实现的过程中,作者总结了一些可以分享的经验:

test/mocks目录中放置所有的 Mock 实现,子目录与接口所在文件的二级目录相同,在这里源文件的位置在pkg/blog/blog.go,它的二级目录就是blog/,所以对应的 Mock 实现会被生成到test/mocks/blog/目录中;指定package为mxxx,默认的mock_xxx看起来非常冗余,上述blog包对应的 Mock 包也就是mblog;

mockgen命令放置到Makefile中的mock下统一管理,减少祖传命令的出现;

mock: rm -rf test/mocks mkdir -p test/mocks/blog mockgen -package=mblog -source=pkg/blog/blog.go > test/mocks/blog/blog.go

当我们生成了上述的 Mock 实现代码之后,就可以使用如下的方式为Service写单元测试了,这段代码通过NewMockBlog生成一个Blog接口的 Mock 实现,然后通过EXPECT方法控制该实现会在调用ListPosts时返回空的Post数组:

func TestListPosts(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockBlog := mblog.NewMockBlog(ctrl) mockBlog.EXPECT().ListPosts().Return([]Post{}) service := NewService(mockBlog) assert.Equal(t, []Post{}, service.ListPosts()) }

由于当前Service只依赖于Blog的实现,所以在这时我们就能够断言当前方法一定会返回[]Post{},这时我们的方法的返回值就只与传入的参数有关(虽然ListPosts方法没有入参),我们能够减少一次关注的上下文并保证测试的稳定和可信。

这是 Go 语言中最标准的单元测试写法,所有依赖的package无论是项目内外都应该使用这种方式处理(在有接口的情况下),如果没有接口 Go 语言的单元测试就会非常难写,这也是为什么从项目中是否有接口就能判断工程质量的原因了。

另一个项目中比较常见的依赖其实就是数据库,在遇到数据库的依赖时,我们一般都会使用sqlmock来模拟数据库的连接,当我们使用 sqlmock 时会写出如下所示的单元测试:

func (s *suiteServerTester) TestRemovePost() { entry := pb.Post{ Id: 1, } rows := sqlmock.NewRows([]string{"id", "author"}).AddRow(1, "draveness") s.Mock.ExpectQuery(`SELECT (.+) FROM "posts"`).WillReturnRows(rows) s.Mock.ExpectExec(`DELETE FROM "posts"`). WithArgs(1). WillReturnResult(sqlmock.NewResult(1, 1)) response, err := s.server.RemovePost(context.Background(), &entry) s.NoError(err) s.EqualValues(response, &entry) s.NoError(s.Mock.ExpectationsWereMet()) }

最常用的几个方法就是ExpectQuery和ExpectExec,前者主要用于模拟 SQL 的查询语句,后者用于模拟 SQL 的增删,从上面的实例中我们可以看到这个这两种方法的使用方式,建议各位先阅读相关的文档再尝试使用。

HTTP 请求也是我们在项目中经常会遇到的依赖,httpmock就是一个用于 Mock 所有 HTTP 依赖的包,它使用模式匹配的方式匹配 HTTP 请求的 URL,在匹配到特定的请求时就会返回预先设置好的响应。

func TestFetchArticles(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() httpmock.RegisterResponder("GET", "https://api.mybiz.com/articles", httpmock.NewStringResponder(200, `[{"id": 1, "name": "My Great Article"}]`)) httpmock.RegisterResponder("GET", `=~^https://api\.mybiz\.com/articles/id/\d+\z`, httpmock.NewStringResponder(200, `{"id": 1, "name": "My Great Article"}`)) ... }

如果遇到 HTTP 请求的依赖时,就可以使用上述httpmock包模拟依赖的 HTTP 请求。

猴子补丁

最后要介绍的猴子补丁其实就是一个大杀器了,bouk/monkey能够通过替换函数指针的方式修改任意函数的实现,所以如果上述的几种方法都不能满足我们的需求,我们就只能够通过猴子补丁这种比较 hack 的方法 Mock 依赖了:

func main() { monkey.Patch(fmt.Println, func(a ...interface{}) (n int, err error) { s := make([]interface{}, len(a)) for i, v := range a { s[i] = strings.Replace(fmt.Sprint(v), "hell", "*bleep*", -1) } return fmt.Fprintln(os.Stdout, s...) }) fmt.Println("what the hell?") // what the *bleep*? }

然而这种方法的使用其实有一些限制,由于它是在运行时替换了函数的指针,所以如果遇到一些简单的函数,例如rand.Int63n和time.Now,编译器可能会直接将这种函数内联到调用实际发生的代码处并不会调用原有的方法,所以使用这种方式往往需要我们在测试时额外指定-gcflags=-l禁止编译器的内联优化。

$go test -gcflags=-l ./...

bouk/monkey的 README 对于它的使用给出了一些注意事项,除了内联编译之外,我们需要注意的是不要在单元测试之外的地方使用猴子补丁,我们应该只在必要的时候使用这种方法,例如依赖的第三方库没有提供interface或者修改time.Now以及rand.Int63n等内置函数的返回值用于测试时。

从理论上来说,通过猴子补丁这种方式我们能够在运行时 Mock Go 语言中的一切函数,这也为我们提供了单元测试 Mock 依赖的最终解决方案。

在最后,我们简单介绍一下辅助单元测试的assert包,它提供了非常多的断言方法帮助我们快速对期望的返回值进行测试,减少我们的工作量:

func TestSomething(t *testing.T) { assert.Equal(t, 123, 123, "they should be equal") assert.NotEqual(t, 123, 456, "they should not be equal") assert.Nil(t, object) if assert.NotNil(t, object) { assert.Equal(t, "Something", object.Value) } }

在这里我们也是简单展示一下assert的示例,更详细的内容可以阅读它的相关文档,在这里也就不多做展示了。

以上就是今天给大家介绍的如何写出优雅的Golang代码如果你还想了解更多关于golang的知识技巧,可以继续关注我们http://www.fastgolang.com

本文地址:http://fastgolang.com/118.html
版权声明:本文为原创文章,版权归 go教练 所有,欢迎分享本文,转载请保留出处!

 发表评论


表情

还没有留言,还不快点抢沙发?