這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
第四章:用GoConvey做測試和mock
我們應該怎樣做微服務的測試?這有什麼特別的挑戰麼.這節,我們將看下面幾點:
- 單元測試
- 用Goconey寫行為模式的單元測試
- 介紹mocking技巧
因為這章不會改變核心服務代碼,所以沒有基測
微服務測試簡介
首先,你必須記住測試金字塔:
單元測試必須作為你整合,e2e的基礎,驗收測試更不容易開發和維護
微服務有一些不同的測試痛點,構建軟體架構用到的準則和做測試一樣.微服務的單元測試和傳統的不太一樣,我們會在這裡講一下.
總之,我強調幾點:
- 做正常地單元測試-你的商業邏輯,驗證器等等不因為在微服務上運行而不同.
- 整合的部分,例如和其他服務溝通,發送資訊,使用資料庫,這些必須用依賴注入的方法來設計,這樣才能用上mock
- 許多微服務的特性:設定檔,其他服務的溝通,彈力測試等.花很多的時間才能做一點測試.這些測試可以做整合測試,你把docker容器整體的做測試.這樣性價比會比較高.
代碼
完整代碼
git checkout P4
介紹
go的單元測試又go的作者設計的遵從語言習慣的模式.測試檔案由命名來識別.如果我們想測試handler.go中的東西,我們建立檔案handlers_test.go,在同一個檔案夾下.
我們從悲觀測試開始,斷言404,當我們請求不存在的地址
package serviceimport ( . "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 dbclientimport ( "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/testifytype 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能在我們每次儲存檔案時自動執行所有測試.我不細講了,這裡看一下代碼覆蓋度報告:
goconvey這種用行為測試方式寫的單元測試並不是每個人都喜歡.有許多其他的測試架構.你能搜尋到很多.
如果我們看測試金字塔上面,我們會想寫整合測試,或者最後的驗收測試.我們之後會啟動真正的boltDb,之後來講一講整合測試.也許會用go docker的遠程api和寫好的boltdb鏡像
總結
這部分我們用goconvey寫第一個單元測試.同事用mock包幫我們類比.
下一節,我們會啟動docker swarm並且部署我們的服務進swarm中