前回は、Go言語のテストの実行方法、カバレッジおよび統合テストについての概略、ベンチマークの実装・実行方法について解説しました。
今回は、APIのテストコードの書き方について、解説します。
Assertの導入
Go言語のtestingパッケージには、Assertが存在しません。
つまり、通常は以下のようにエラー判定を行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// sort.go package sort ... func getElements(n int) []int { result := make([]int, n) j := 0 for i := n; i > 0; i-- { result[j] = i-1 j++ } return result } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// sort_test.go package sort import ( "testing" ) func TestGetElementsFunction(t *testing.T) { elements := GetElements(10) if len(elements) != 10 { t.Error("elements should contains 10 elements") } if elements[0] != 9 { t.Error("first element should be 9") } if elements[9] != 0 { t.Error("last element should be 0") } } |
テストを実行します。
1 2 3 4 |
$ go test -run GetElementsFunction testing: warning: no tests to run PASS ok github.com/hoge/golang-testing/api/services 0.009s |
これでもOKなのですが、Pythonなど他の言語に触れてきた人たちにとっては何かが足りません。
そう、Assertionがないのです。
Go言語では、Assertionはデフォルトで組み込まれていないため、別途モジュールをインストールする必要があります。
今回は、「stretchr/testify」パッケージを利用します。
以下のパッケージをインストールしてください。
1 |
go get "github.com/stretchr/testify/assert" |
このパッケージを利用して、先ほどのテストを書き直してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// sort_test.go package sort import ( "testing" "github.com/stretchr/testify/assert" ) func TestGetElementsFunctionWithAssertion(t *testing.T) { elements := GetElements(10) assert.EqualValues(t, 10, len(elements)) assert.EqualValues(t, 9, elements[0]) assert.EqualValues(t, 0, elements[9]) } |
だいぶスッキリしましたね。
テストを実行してみましょう。
1 2 3 |
$ go test -run GetElementsFunctionWithAssertion PASS ok github.com/hoge/golang-testing/api/utils/sort 0.025s |
Passしました!
Rest API Test
以下のパッケージをインストールしてください。
1 |
go get github.com/mercadolibre/golang-restclient/rest |
本テストは、以下のAPIを利用します。
また、以下のフォルダとファイルを作成してください。
1 2 3 4 5 6 7 8 |
mkdir -p domain/locations touch domain/locations/country.go mkdir -p providers/locations_provider touch providers/locations_provider/locations_provider.go touch api/providers/locations_provider/locations_provider_test.go mkdir api/utils/errors touch api/utils/errors/api_error.go touch api/main.go |
まず、エラー格納用のStructを作成しましょう。
1 2 3 4 5 6 7 8 9 |
// api_error.go package errors type ApiError struct { Status int `json:"status"` Message string `json:"message"` Error string `json:"error"` } |
続いて、Country APIから取得した値を格納するためのStructを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// country.go package locations type Country struct { ID string `json:"id"` Name string `json:"name"` TimeZone string `json:"time_zone"` GeoInformation GeoInformation `json:"geo_information"` States []State `json:"states"` } type GeoInformation struct { Location GeoLocation `json:"location"` } type GeoLocation struct { Latitude float64 `json:"latitude"` Longitute float64 `json:"longitude"` } type State struct { ID string `json:"id"` Name string `json:"name"` } |
続いて、CountryAPIから値を取得する関数を作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
// locations_provider.go package locations_provider import ( "encoding/json" "fmt" "net/http" "github.com/mercadolibre/golang-restclient/rest" "github.com/hoge/golang-testing/api/domain/locations" "github.com/hoge/golang-testing/api/utils/errors" ) const ( urlGetCountry = "https://api.mercadolibre.com/countries/%s" ) // GetCountry - provides the function to get country info func GetCountry(countryID string) (*locations.Country, *errors.ApiError) { response := rest.Get(fmt.Sprintf(urlGetCountry, countryID)) if response == nil || response.Response == nil { return nil, &errors.ApiError{ Status: http.StatusInternalServerError, Message: fmt.Sprintf("invalid restclient response when trying to get country %s", countryID), } } if response.StatusCode != http.StatusOK { var apiErr errors.ApiError if err := json.Unmarshal(response.Bytes(), &apiErr); err != nil { return nil, &errors.ApiError{ Status: http.StatusInternalServerError, Message: fmt.Sprintf("invalid error response when getting country %s", countryID), } } return nil, &apiErr } var result locations.Country if err := json.Unmarshal(response.Bytes(), &result); err != nil { return nil, &errors.ApiError{ Status: http.StatusInternalServerError, Message: fmt.Sprintf("error when trying to unmarshal country data for %s", countryID), } } return &result, nil } |
正常に処理が走るか確かめてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package main import ( "fmt" "github.com/hoge/golang-testing/api/providers/locations_provider" ) func main() { country, _ := locations_provider.GetCountry("AR") fmt.Println(country) _, err := locations_provider.GetCountry("ARS") fmt.Println(err) _, err = locations_provider.GetCountry("") fmt.Println(err) } |
1 2 3 4 |
$ go run main.go &{AR Argentina GMT-03:00 {{-38.416096 -63.616673}} [{AR-B Buenos Aires} {AR-C Capital Federal} {AR-K Catamarca} {AR-H Chaco} {AR-U Chubut} {AR-W Corrientes} {AR-X Córdoba} {AR-E Entre Ríos} {AR-P Formosa} {AR-Y Jujuy} {AR-L La Pampa} {AR-F La Rioja} {AR-M Mendoza} {AR-N Misiones} {AR-Q Neuquén} {AR-R Río Negro} {AR-A Salta} {AR-J San Juan} {AR-D San Luis} {AR-Z Santa Cruz} {AR-S Santa Fe} {AR-G Santiago del Estero} {AR-V Tierra del Fuego} {AR-T Tucumán}]} &{404 Country not found not_found} &{500 error when trying to unmarshal country data for } |
OKです。
では、テストを書いていきましょう。
最初は、API問い合わせテストです。このテストは、Wifiを切らないと(インターネット接続を切らないと)テストできません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// locations_provider_test.go package locations_provider import ( "net/http" "testing" "github.com/stretchr/testify/assert" ) // このテストは、Wifiを切らないとテストできない func TestGetCountryRestclientError(t *testing.T) { // Init: // Execute: country, err := GetCountry("AR") // Validation: assert.Nil(t, country) assert.NotNil(t, err) assert.EqualValues(t, http.StatusInternalServerError, err.Status) assert.EqualValues(t, "invalid restclient response when trying to get country AR", err.Message) } |
テストを実行してみます。
1 2 3 4 5 6 7 |
$ go test -run TestGetCountryRestclientError flag provided but not defined: -test.timeout Usage of /var/folders/zx/vr9x_3l51g3d02wd9dl7_vkw0000gn/T/go-build395454584/b001/locations_provider.test: -mock Use 'mock' flag to tell package rest that you would like to use mockups. exit status 2 FAIL github.com/hoge/golang-testing/api/providers/locations_provider 0.048s |
「Use ‘mock’ flag to tell package rest that you would like to use mockups.」という想定外のエラーが出てしまいました。
これは、mercadolibre/golang-restclientの既知のバグのようです。
暫定対処として、mercadolibre/golang-restclientの以下のファイルの処理をコメントアウトします。
1 2 3 4 5 6 7 8 9 |
// mockup.go func init() { flag.BoolVar(&mockUpEnv, "mock", false, "Use 'mock' flag to tell package rest that you would like to use mockups.") // flag.Parse() <<<< ここをコメントアウト startMockupServ() } |
もう一度テストを実行します。
1 2 3 |
$ go test -run TestGetCountryRestclientError PASS ok github.com/test/golang-testing/api/providers/locations_provider 0.023s |
テストが成功しました。
続いて、指定したCountryが存在しない場合のテストです。
1 2 3 4 5 6 7 8 9 10 11 12 |
func TestGetCountryNotFound(t *testing.T) { // Init: // Execute: country, err := GetCountry("ARS") // Validation: assert.Nil(t, country) assert.NotNil(t, err) assert.EqualValues(t, http.StatusNotFound, err.Status) assert.EqualValues(t, "Country not found", err.Message) } |
1 2 3 |
go test -run TestGetCountryNotFound PASS ok github.com/test/golang-testing/api/providers/locations_provider 0.854s |
次は、Countryを指定していなかった場合のテストです。
1 2 3 4 5 6 7 8 9 10 11 12 |
func TestGetCountryInvalidErrorInterface(t *testing.T) { // Init: // Execute: country, err := GetCountry("") // Validation: assert.Nil(t, country) assert.NotNil(t, err) assert.EqualValues(t, http.StatusInternalServerError, err.Status) assert.EqualValues(t, "error when trying to unmarshal country data for ", err.Message) } |
1 2 3 |
$ go test -run TestGetCountryInvalidErrorInterface PASS ok github.com/test/golang-testing/api/providers/locations_provider 1.068s |
最後にCountryが正しく取得できるかテストします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
func TestGetCountryNoError(t *testing.T) { // Init: // Execute: country, err := GetCountry("AR") // Validation: assert.Nil(t, err) assert.NotNil(t, country) // Validation: // Api Contents // https://api.mercadolibre.com/countries/AR assert.EqualValues(t, "AR", country.ID) assert.EqualValues(t, "Argentina", country.Name) assert.EqualValues(t, "GMT-03:00", country.TimeZone) assert.EqualValues(t, 24, len(country.States)) } |
1 2 3 |
$ go test -run TestGetCountryNoError PASS ok github.com/hoge/golang-testing/api/providers/locations_provider 0.770s |
OKですね。
Mockテスト
続いて、Mockテストを行いましょう。
Mockは、外部機能をシミュレーションするためのテスト手法の一つです。
先ほどの「TestGetCountryRestclientError」関数のInitにMock処理を書きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
func TestGetCountryRestclientError(t *testing.T) { // Init: rest.StartMockupServer() // Setup Mock Parameter rest.AddMockups(&rest.Mock{ URL: "https://api.mercadolibre.com/countries/AR", HTTPMethod: http.MethodGet, RespHTTPCode: 0, }) // Execute: country, err := GetCountry("AR") // Validation: assert.Nil(t, country) assert.NotNil(t, err) assert.EqualValues(t, http.StatusInternalServerError, err.Status) assert.EqualValues(t, "invalid restclient response when trying to get country AR", err.Message) } |
StartMockupServer関数でMockを初期化し、その後、AddMockupsで、シミュレーションしたいパラメータ情報を渡します。
今回は、実際に存在するURL「https://api.mercadolibre.com/countries/AR」を渡していますが、開発の状況によっては呼び出し先のAPIが作られていないこともあります。
そんな時に、Mockを使えば擬似的なシミュレーションができるというわけです。
テストを実行します。
1 2 3 |
$ go test -run TestGetCountryRestclientError PASS ok github.com/hoge/golang-testing/api/providers/locations_provider 0.021s |
Passしましたね。思い出していただきたいのですが、このテストはWifiを切らないとテストできないものでしたが、Mockに置き換えたことでテスト可能になりました。
他の関数もMockに置き換えていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
func TestGetCountryNotFound(t *testing.T) { // Init: rest.StartMockupServer() rest.AddMockups(&rest.Mock{ URL: "https://api.mercadolibre.com/countries/AR", HTTPMethod: http.MethodGet, RespHTTPCode: http.StatusNotFound, RespBody: `{"message": "Country not found", "error": "not_found", "status": 404, "cause": []}`, }) // Execute: country, err := GetCountry("AR") // Validation: assert.Nil(t, country) assert.NotNil(t, err) assert.EqualValues(t, http.StatusNotFound, err.Status) assert.EqualValues(t, "Country not found", err.Message) } |
1 2 3 |
$ go test -run TestGetCountryNotFound PASS ok github.com/hoge/golang-testing/api/providers/locations_provider 0.028s |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
func TestGetCountryInvalidErrorInterface(t *testing.T) { // Init: rest.StartMockupServer() rest.AddMockups(&rest.Mock{ URL: "https://api.mercadolibre.com/countries/AR", HTTPMethod: http.MethodGet, RespHTTPCode: http.StatusNotFound, RespBody: `{"message": "Country not found", "error": "not_found", "status": "404", "cause": []}`, }) // Execute: country, err := GetCountry("") // Validation: assert.Nil(t, country) assert.NotNil(t, err) assert.EqualValues(t, http.StatusInternalServerError, err.Status) assert.EqualValues(t, "invalid error response when getting country ", err.Message) } |
1 2 3 |
$ go test -run TestGetCountryInvalidErrorInterface PASS ok github.com/hoge/golang-testing/api/providers/locations_provider 0.027s |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
func TestGetCountryInvalidJsonResponse(t *testing.T) { // Init: rest.StartMockupServer() rest.AddMockups(&rest.Mock{ URL: "https://api.mercadolibre.com/countries/AR", HTTPMethod: http.MethodGet, RespHTTPCode: http.StatusOK, RespBody: `{"id": 123, "name": "Argentina", "time_zone": "GMT-03:00"}`, }) // Execute: country, err := GetCountry("AR") // Validation: assert.Nil(t, country) assert.NotNil(t, err) assert.EqualValues(t, http.StatusInternalServerError, err.Status) assert.EqualValues(t, "error when trying to unmarshal country data for AR", err.Message) } |
1 2 3 |
$ go test -run TestGetCountryInvalidJsonResponse PASS ok github.com/hoge/golang-testing/api/providers/locations_provider 0.029s |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
func TestGetCountryNoError(t *testing.T) { // Init: rest.StartMockupServer() rest.AddMockups(&rest.Mock{ URL: "https://api.mercadolibre.com/countries/AR", HTTPMethod: http.MethodGet, RespHTTPCode: http.StatusOK, // https://api.mercadolibre.com/countries/AR // https://www.cleancss.com/json-minify/ RespBody: `{"id":"AR","name":"Argentina","locale":"es_AR","currency_id":"ARS","decimal_separator":",","thousands_separator":".","time_zone":"GMT-03:00","geo_information":{"location":{"latitude":-38.416096,"longitude":-63.616673}},"states":[{"id":"AR-B","name":"Buenos Aires"},{"id":"AR-C","name":"Capital Federal"},{"id":"AR-K","name":"Catamarca"},{"id":"AR-H","name":"Chaco"},{"id":"AR-U","name":"Chubut"},{"id":"AR-W","name":"Corrientes"},{"id":"AR-X","name":"Córdoba"},{"id":"AR-E","name":"Entre Ríos"},{"id":"AR-P","name":"Formosa"},{"id":"AR-Y","name":"Jujuy"},{"id":"AR-L","name":"La Pampa"},{"id":"AR-F","name":"La Rioja"},{"id":"AR-M","name":"Mendoza"},{"id":"AR-N","name":"Misiones"},{"id":"AR-Q","name":"Neuquén"},{"id":"AR-R","name":"Río Negro"},{"id":"AR-A","name":"Salta"},{"id":"AR-J","name":"San Juan"},{"id":"AR-D","name":"San Luis"},{"id":"AR-Z","name":"Santa Cruz"},{"id":"AR-S","name":"Santa Fe"},{"id":"AR-G","name":"Santiago del Estero"},{"id":"AR-V","name":"Tierra del Fuego"},{"id":"AR-T","name":"Tucumán"}]}`, }) // Execute: country, err := GetCountry("AR") // Validation: assert.Nil(t, err) assert.NotNil(t, country) // Validation: // Api Contents // https://api.mercadolibre.com/countries/AR assert.EqualValues(t, "AR", country.ID) assert.EqualValues(t, "Argentina", country.Name) assert.EqualValues(t, "GMT-03:00", country.TimeZone) assert.EqualValues(t, 24, len(country.States)) } |
1 2 3 |
$ go test -run TestGetCountryNoError PASS ok github.com/hoge/golang-testing/api/providers/locations_provider 0.024s |
全てのテストに「rest.StartMockupServer()」を入れてますが、TestMainに入れれば、他の言語のSetUp関数のようにテスト開始時に実行してくれるようになります。
1 2 3 4 |
func TestMain(m *testing.M) { rest.StartMockupServer() os.Exit(m.Run()) } |
また、それぞれのテストに次の処理を入れておきましょう。
1 2 3 4 |
func TestGetCountryRestclientError(t *testing.T) { // Init: rest.FlushMockups() } |
FlushMockupsについてはの詳しい説明はありませんでしたが、以下の処理からそれぞれのテスト間で、Mockのロックを行うようです。
1 2 3 4 5 6 |
// FlushMockups ... func FlushMockups() { mockDbMutex.Lock() mockMap = make(map[string]*Mock) mockDbMutex.Unlock() } |
次回
今回は、Assertionの導入、REST APIのテスト、Mockテストについて解説しました。
次回はもっと包括的なテストを行います!
コメントを残す
コメントを投稿するにはログインしてください。