基於go的微服務搭建(四)- 用GoConvey作測試和mock

第四章:用GoConvey作測試和mockgit

咱們應該怎樣作微服務的測試?這有什麼特別的挑戰麼.這節,咱們將看下面幾點:github

  • 單元測試
  • 用Goconey寫行爲模式的單元測試
  • 介紹mocking技巧

由於這章不會改變核心服務代碼,因此沒有基測docker

微服務測試簡介


首先,你必須記住測試金字塔:數據庫

clipboard.png

單元測試必須做爲你集成,e2e的基礎,驗收測試更不容易開發和維護
微服務有一些不一樣的測試難點,構建軟件架構用到的準則和作測試同樣.微服務的單元測試和傳統的不太同樣,咱們會在這裏講一下.
總之,我強調幾點:json

  • 作正常地單元測試-你的商業邏輯,驗證器等等不由於在微服務上運行而不一樣.
  • 集成的部分,例如和其餘服務溝通,發送信息,使用數據庫,這些必須用依賴注入的方法來設計,這樣才能用上mock
  • 許多微服務的特性:配置文件,其餘服務的溝通,彈力測試等.花不少的時間才能作一點測試.這些測試能夠作集成測試,你把docker容器總體的作測試.這樣性價比會比較高.

代碼


完整代碼api

git checkout P4

介紹


go的單元測試又go的做者設計的聽從語言習慣的模式.測試文件由命名來識別.若是咱們想測試handler.go中的東西,咱們建立文件handlers_test.go,在同一個文件夾下.
咱們從悲觀測試開始,斷言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)
                        })
                })
        })
}

這個測試顯示"Given-when-then"(若是-當-推斷)的模式.咱們也用httptest包,咱們用它來聲明請求的object也用作回覆的object用來做爲斷言的條件.
去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/service    0.012s

./...會運行當前文件夾和全部子文件夾下的測試文件.咱們也能夠進入service文件夾下go test,這會運行這個文件夾下的測試.函數

Mocking


上面的測試不須要mock,由於咱們不會用到GetAccount裏面的DBClient.對於好的請求,咱們須要返回結果,咱們就須要mock客戶端來鏈接BoltDb.有許多mocking的方法.我最喜歡的是stretchr/testify/mock這個包
在/dbclient文件夾下,建立mockclient.go來實現IBoltClient接口:微服務

package dbclient

import (
        "github.com/stretchr/testify/mock"
        "github.com/callistaenterprise/goblog/accountservice/model"
)

// MockBoltClient is a mock implementation of a datastore client for testing purposes.
// Instead of the bolt.DB pointer, we're just putting a generic mock object from
// strechr/testify
type MockBoltClient struct {
        mock.Mock
}

// From here, we'll declare three functions that makes our MockBoltClient fulfill the interface IBoltClient that we declared in part 3.
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() {
        // Does nothing
}

func (m *MockBoltClient) Seed() {
        // Does nothing
}

MockBoltClient如今能夠做爲咱們能夠編寫的mock.向上邊那樣,咱們隱式的定義了全部的函數,實現IBoltClient接口.
若是你不喜歡這樣的mock方法,能夠看一下mockery,他能夠產生任何go接口的mock
QueryAccount函數裏有點奇怪.但這就是testify的作法,這樣能讓咱們有一個全面的內部控制的mock.

編寫mock


咱們建立下一個測試函數在handlers_test.go中:

func TestGetAccount(t *testing.T) {
        // Create a mock instance that implements the IBoltClient interface
        mockRepo := &dbclient.MockBoltClient{}

        // Declare two mock behaviours. For "123" as input, return a proper Account struct and nil as error.
        // For "456" as input, return an empty Account object and a real error.
        mockRepo.On("QueryAccount", "123").Return(model.Account{Id:"123", Name:"Person_123"}, nil)
        mockRepo.On("QueryAccount", "456").Return(model.Account{}, fmt.Errorf("Some error"))
        
        // Finally, assign mockRepo to the DBClient field (it's in _handlers.go_, e.g. in the same package)
        DBClient = mockRepo
        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")
                })
        })
})
}

這段測試請求path/accounts/123,咱們的mock實現了這個.在when中,咱們斷言http狀態,反序列化Account結構,同時段驗結果和咱們的mock的結果相同.
我喜歡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/service    0.026s

全綠!goconvey有一個GUI能在咱們每次保存文件時自動執行全部測試.我不細講了,這裏看一下代碼覆蓋度報告:

clipboard.png
goconvey這種用行爲測試方式寫的單元測試並非每一個人都喜歡.有許多其餘的測試框架.你能搜索到不少.
若是咱們看測試金字塔上面,咱們會想寫集成測試,或者最後的驗收測試.咱們以後會啓動真正的boltDb,以後來說一講集成測試.也許會用go docker的遠程api和寫好的boltdb鏡像

總結


這部分咱們用goconvey寫第一個單元測試.同事用mock包幫咱們模擬.下一節,咱們會啓動docker swarm而且部署咱們的服務進swarm中

相關文章
相關標籤/搜索