走近微服务,第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 条评论
登录 后参与评论

相关文章

来自专栏落花落雨不落叶

canvas画简单电路图

60811
来自专栏java 成神之路

使用 NIO 实现 echo 服务器

4597
来自专栏张善友的专栏

Mix 10 上的asp.net mvc 2的相关Session

Beyond File | New Company: From Cheesy Sample to Social Platform Scott Hansel...

2547
来自专栏C#

DotNet加密方式解析--非对称加密

    新年新气象,也希望新年可以挣大钱。不管今年年底会不会跟去年一样,满怀抱负却又壮志未酬。(不过没事,我已为各位卜上一卦,卦象显示各位都能挣钱...)...

4848
来自专栏Golang语言社区

【Golang语言社区】GO1.9 map并发安全测试

var m sync.Map //全局 func maintest() { // 第一个 YongHuomap := make(map[st...

4708
来自专栏闻道于事

js登录滑动验证,不滑动无法登陆

js的判断这里是根据滑块的位置进行判断,应该是用一个flag判断 <%@ page language="java" contentType="text/html...

6768
来自专栏陈仁松博客

ASP.NET Core 'Microsoft.Win32.Registry' 错误修复

今天在发布Asp.net Core应用到Azure的时候出现错误InvalidOperationException: Cannot find compilati...

4838
来自专栏pangguoming

Spring Boot集成JasperReports生成PDF文档

由于工作需要,要实现后端根据模板动态填充数据生成PDF文档,通过技术选型,使用Ireport5.6来设计模板,结合JasperReports5.6工具库来调用渲...

1.2K7
来自专栏张善友的专栏

Silverlight + Model-View-ViewModel (MVVM)

     早在2005年,John Gossman写了一篇关于Model-View-ViewModel模式的博文,这种模式被他所在的微软的项目组用来创建Expr...

2948
来自专栏一个会写诗的程序员的博客

Spring Reactor 项目核心库Reactor Core

Non-Blocking Reactive Streams Foundation for the JVM both implementing a Reactiv...

2142

扫码关注云+社区