走近微服务,第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技术

2018整理最全的50道Redis面试题!

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

890
来自专栏程序员与猫

JSON Patch

966
来自专栏Java技术栈

dubbo服务调试管理实用命令

公司如果分项目组开发的,各个项目组调用各项目组的接口,有时候需要在联调环境调试对方的接口,可以直接telnet到dubbo的服务通过命令查看已经布的接口和方法,...

3467
来自专栏Java进阶架构师

「mysql优化专题」这大概是一篇最好的mysql优化入门文章(1)

优化,一直是面试最常问的一个问题。因为从优化的角度,优化的思路,完全可以看出一个人的技术积累。那么,关于系统优化,假设这么个场景,用户反映系统太卡(其实就是高并...

764
来自专栏IMWeb前端团队

给react加try-catch

最近在一个使用fis构建的react.js项目里遇到个问题,render函数里如果发生了运行时错误,比如说某个对象没有判断就直接去访问其属性,那我所知道的就是,...

2605
来自专栏用户2442861的专栏

Redis作者谈Redis应用场景

毫无疑问,Redis开创了一种新的数据存储思路,使用Redis,我们不用在面对功能单调的数据库时,把精力放在如何把大象放进冰箱这样的问题上,而是利用Redis...

722
来自专栏铭毅天下

干货 | Elasticsearch索引生命周期管理探索

Elasticsearch上海Meetup中ebay工程师提了索引生命周期管理的概念。的确,在Demo级别的验证阶段我们数据量比较小,不太需要关注索引的生命周期...

692
来自专栏信安之路

轻松理解什么是 SQL 注入

作为长期占据 OWASP Top 10 首位的注入,OWASP 对于注入的解释如下:

670
来自专栏salesforce零基础学习

salesforce零基础学习(七十二)项目中的零碎知识点小总结(一)

项目终于告一段落,虽然比较苦逼,不过也学到了好多知识,总结一下,以后当作参考。 一.visualforce标签中使用html相关的属性使用 曾经看文档没有看得仔...

19910
来自专栏Java架构师历程

MYSQL 谈谈各存储引擎的优缺点

1、存储引擎其实就是如何实现存储数据,如何为存储的数据建立索引以及如何更新,查询数据等技术实现的方法。

882

扫码关注云+社区