应该如何测试微服务?在为这个特定领域制定测试方案时,需要考虑哪些特别的挑战?在本博客系列的第4部分中,我们将一窥究竟。
由于这部分不会以任何方式改变核心服务,所以这次没有基准。
首先,应该牢记测试金字塔的原则。
由于集成测试,系统测试和验收测试的开发和维护成本越来越高,因此应该以单元测试应该构成大部分测试。
其次 - 微服务无疑带来了一些特别的测试难题,其中的一部分就像在实际测试中使用合理的原则为服务实现建立软件架构时一样。这就是说 - 我认为很多具体的微服务超出了传统单元测试的范畴,我们将在博客系列的这部分中处理这些内容。
无论如何,我想强调几点:
和以前一样,你可以从克隆的存储库检测出适当的分支,得到本部分的完整源代码:
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最终启动并运行了,并将我们一直在使用的微服务部署到群集中。