走近微服务,第4部分:使用GoConvey进行测试和模拟

应该如何测试微服务?在为这个特定领域制定测试方案时,需要考虑哪些特别的挑战?在本博客系列的第4部分中,我们将一窥究竟。

  • 在单元环境中测试微服务的主题
  • GoConvey的BDD风格编写单元测试
  • 引入模拟技术

由于这部分不会以任何方式改变核心服务,所以这次没有基准。

首先,应该牢记测试金字塔的原则。

测试金字塔

由于集成测试,系统测试和验收测试的开发和维护成本越来越高,因此应该以单元测试应该构成大部分测试。

其次 - 微服务无疑带来了一些特别的测试难题,其中的一部分就像在实际测试中使用合理的原则为服务实现建立软件架构时一样。这就是说 - 我认为很多具体的微服务超出了传统单元测试的范畴,我们将在博客系列的这部分中处理这些内容。

无论如何,我想强调几点:

  • 像平常一样进行单元测试 -不要仅仅因为它们在微服务环境中运行,就认为您的业务逻辑,转换器,验证器等等有什么特殊之处。
  • 集成组件如(用于与其他服务进行通信,发送消息,访问数据库等的)客户端,应该设计依赖注入,考虑可模拟性。
  • 许多微服务细节 ——访问配置,与其他服务交流,弹性测试等等——对于一个非常小的值,需要大量的时间。将这些测试保存到类似集成的测试中,通过测试代码启动像Docker容器一样的依赖服务。它将提供更大的价值,并且可能更容易启动和运行。

源代码

和以前一样,你可以从克隆的存储库检测出适当的分支,得到本部分的完整源代码:

git checkout P4

介绍

Go中的单元测试遵循由Go作者建立的一些惯用模式。测试源文件通过命名约定来标识。例如,如果我们想在我们的handlers.go文件中测试一些东西,我们会在同一个目录下创建文件handlers_test.go。让我们行动起来吧。

我们将从一个不可达的路径测试开始,如果我们请求一个未知的路径,我们会得到一个HTTP 404:

package service
import (
        . "github.com/smartystreets/goconvey/convey"
        "testing"
        "net/http/httptest"
)
func TestGetAccountWrongPath(t *testing.T) {
        Convey("Given a HTTP request for /invalid/123", t, func() {
                req := httptest.NewRequest("GET", "/invalid/123", nil)
                resp := httptest.NewRecorder()
                Convey("When the request is handled by the Router", func() {
                        NewRouter().ServeHTTP(resp, req)
                        Convey("Then the response should be a 404", func() {
                                So(resp.Code, ShouldEqual, 404)
                        })
                })
        })
}

此测试显示了GoConvey的“When-Then-Then”行为驱动结构,以及“So A ShouldEqual B”声明样式。它还介绍了httptest包的用法,我们使用它来声明请求对象以及响应对象,以便执行命令。

通过移动到根文件夹“accountservice”运行它并键入:

> go test ./...
?   github.com/callistaenterprise/goblog/accountservice[no test files]
?   github.com/callistaenterprise/goblog/accountservice/dbclient[no test files]
?   github.com/callistaenterprise/goblog/accountservice/model[no test files]
ok  github.com/callistaenterprise/goblog/accountservice/service0.012s

想知道"/ ..."是什么意思吗?这是告诉go测试在当前文件夹所有子文件夹中运行所有测试。我们也可以进入“服务”文件夹并键入go test,然后只会在该文件夹中执行测试。

由于“服务”软件包是唯一一个包含测试文件的软件包,其他软件包报告其中没有测试。这很好,至少现在很好!

模拟

我们上面创建的测试不需要模拟任何东西,因为实际的调用不会到达我们的GetAccount函数,它依赖于我们在第3部分中创建的DBClient 。对于我们实际想要返回某些内容的良好的路径测试,无论如何,我们需要模拟正在使用的客户端来访问BoltDB。关于如何在Go中进行模拟有很多策略。我将使用拉伸器/证明/模拟软件包展示我最喜欢的一种方式。

/ dbclient文件夹中,创建一个名为mockclient.go的新文件,它将成为我们的IBoltClient接口的实现。

package dbclient
import (
        "github.com/stretchr/testify/mock"
        "github.com/callistaenterprise/goblog/accountservice/model"
)
// MockBoltClient 是一个用于测试的数据存储客户端的模拟实现.
// 我们仅仅放置一个通用模拟对象,而不是bolt.DB 指针
// strechr/testify
type MockBoltClient struct {
        mock.Mock
}
// 这里, 我们将定义三个函数使我们的 MockBoltClient 满足第3部分中定义的 IBoltClient 接口(功能).
func (m *MockBoltClient) QueryAccount(accountId string) (model.Account, error) {
        args := m.Mock.Called(accountId)
        return args.Get(0).(model.Account), args.Error(1)
}
func (m *MockBoltClient) OpenBoltDb() {
        //什么也不做
}
func (m *MockBoltClient) Seed() {
        // 什么也不做
}

MockBoltClient现在可以用作我们明确定制的可编程模拟。如上所述,由于MockBoltClient结构具有与IBoltClient接口中声明的所有函数的签名相匹配的函数,因此此代码隐式实现了IBoltClient接口。

如果你不喜欢为你的模拟写样板代码,我建议看一看Mockery,它可以为任何Go界面弄生成模拟。

QueryAccount函数体看起来可能有些奇怪,但它只是简单地说明“strechr/testify”如何为我们提供一个可编程模拟,并且我们可以完全控制其内部机制。

编程模拟

让我们在handlers_test.go中创建另一个测试函数:

func TestGetAccount(t *testing.T) {
        // 创建一个实现IBoltClient 接口的模拟示例
        mockRepo := &dbclient.MockBoltClient{}
        // 定义两个模拟行为。 如输入“123”, 返回一个适当的Account 结构体和零错误。
        // 对于输入“456”, 返回一个空的Account对象和真正的错误.
        mockRepo.On("QueryAccount", "123").Return(model.Account{Id:"123", Name:"Person_123"}, nil)
        mockRepo.On("QueryAccount", "456").Return(model.Account{}, fmt.Errorf("Some error"))
        // 最后, 将mockRepo安排到DBClient字段 (它在_handlers.go_文件中, e.g. in the same package)
        DBClient = mockRepo
        ...
}

接下来,用另一个GoConvey测试替换上述...:

Convey("Given a HTTP request for /accounts/123", t, func() {
        req := httptest.NewRequest("GET", "/accounts/123", nil)
        resp := httptest.NewRecorder()
        Convey("When the request is handled by the Router", func() {
                NewRouter().ServeHTTP(resp, req)
                Convey("Then the response should be a 200", func() {
                        So(resp.Code, ShouldEqual, 200)
                        account := model.Account{}
                        json.Unmarshal(resp.Body.Bytes(), &account)
                        So(account.Id, ShouldEqual, "123")
                        So(account.Name, ShouldEqual, "Person_123")
                })
        })
})

该测试执行一个对已知路径“/accounts/123”的请求,我们的模拟知道这个路径。在“When”块中,我们声明HTTP状态,解析返回的Account结构体和声明,这些字段与我们要求模拟返回的内容相匹配。

我喜欢GoConvey和Given-When-Then编写测试的方式是因此它们非常易于阅读并且具有很好的结构。

我们不妨在请求“/accounts/456”的地方添加一个不可达路径,并且声明我们得到返回值HTTP404:

Convey("Given a HTTP request for /accounts/456", t, func() {
        req := httptest.NewRequest("GET", "/accounts/456", nil)
        resp := httptest.NewRecorder()
        Convey("When the request is handled by the Router", func() {
                NewRouter().ServeHTTP(resp, req)
                Convey("Then the response should be a 404", func() {
                        So(resp.Code, ShouldEqual, 404)
                })
        })
})

再次运行我们的测试,结束。

> go test ./...
?   github.com/callistaenterprise/goblog/accountservice[no test files]
?   github.com/callistaenterprise/goblog/accountservice/dbclient[no test files]
?   github.com/callistaenterprise/goblog/accountservice/model[no test files]
ok  github.com/callistaenterprise/goblog/accountservice/service0.026s

全绿!GoConvey实际上有一个交互式GUI,可以在我们每次保存文件时执行所有测试。我不会详细介绍它,但看起来像这样,还提供了诸如自动代码覆盖率报告之类的内容:

这些GoConvey测试是单元测试,但不是每个人都喜欢通过BDD风格编写它们。Golang还有许多其他测试框架,使用你最喜爱的搜索引擎进行快速搜索可能会产生许多有趣的选项。

如果我们将测试金字塔向上移动,我们将要编写集成测试,最后是验收测试,或许使用诸如Cucumber之类的技术。那已经超出了我们现在讨论的范围,但是我们希望稍后回到编写集成测试的主题上。我们将在测试代码中实际引导一个真正的BoltDB,也许通过使用Go Docker Remote API和预先处理的BoltDB映像。

另一种集成测试方法是自动部署码头化的微服务格局。请参阅我去年写的博客文章,其中我用了一个小的Go程序,根据.yaml规范引导所有微服务,包括支持服务,然后对这些服务执行少数HTTP调用以确保部署的正确性。

在这一部分,我们编写了我们的第一个部分——单元测试,使用第三方GoConvey 和 stretchr/testify/mock”帮助我们。我们将在本博客系列 的后面部分进行更多测试。

接下来的部分中,是时候让Docker Swarm最终启动并运行了,并将我们一直在使用的微服务部署到群集中。

本文的版权归 用户2176511 所有,如需转载请联系作者。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏JAVA技术zhai

分享30道Redis面试题,面试官能问到的我都找到了

Redis本质上是一个Key-Value类型的内存数据库,很像memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到...

2182
来自专栏程序员互动联盟

【线程池】线程池与工作队列

为什么要用线程池? 诸如 Web 服务器、数据库服务器、文件服务器或邮件服务器之类的许多服务器应用程序都面向处理来自某些远程来源的大量短小的任务。请求以某种方式...

3478
来自专栏个人分享

数据集成中间件知识点总结

  数据集成是把不同来源、格式、特点性质的数据在逻辑上或物理上有机地集中,从而为企业提供全面的数据共享。

2931
来自专栏zingpLiu

tcpdump命令

  tcpdump是Linux下强大的抓包工具,不仅可以分析数据包流向,还可以对数据包内容进行监听。通过分析数据包流向,可以了解一条连接是如何建立双向连接的。 ...

1092
来自专栏python3

python3--进程

进程的概念起源于操作系统,是操作系统最核心的概念,也是操作系统提供的最古老也是最重要的抽象概念之一。操作系统的其他所有内容都是围绕进程的概念展开的

932
来自专栏CSDN技术头条

关于缓存你需要知道的

About Cache 作后端开发的同学,缓存是必备技能。这是你不需要花费太多的精力就能显著提升服务性能的灵丹妙药。前提是你得知道如何使用它,这样才能够最大限度...

2117
来自专栏进击的程序猿

ZooKeeper: Wait-free coordination for Internet-scale systems(笔记)

本文是读ZooKeeper: Wait-free coordination for Internet-scale systems的笔记,从第一手资料了解zook...

933
来自专栏北京马哥教育

HTTP/2 十分钟速知

升级到 HTTP/2 后,那些针对HTTP/1.x 的优化手段需要如何变化? 答:总结来说,除了多域名增加并行 TCP 连接数不再适用以外,启用 HTTP/2...

3988
来自专栏码神联盟

灵丹妙药 | 关于缓存,你必须要知道的

这两天小编一直在总结缓存的要点,也同时参考了一些文档,仅此奉上,以供参考。 缓存是必备技能 身为后端开发的开发人员,缓存是必备技能。不需要花费太多的精力就能显著...

3457
来自专栏Java技术栈

秒杀系统设计的 5 个要点:前端三板斧+后端两条路!

9962

扫码关注云+社区