TL;DR
- golang でのテストのお話
- Cloud Datastore は gRPC なので
http.Client
の変更でモック化する httpmock とかが使えない - Cloud Datastore Emulator を GitLab CI の services として起動しておけばテストできる
ことの発端
以下のようなプログラムがあったとして、 httpmock を利用して Cloud Datastore へのリクエストをモック化してテストコードを書こうとしたら datastore.Client の作成でエラーが出た.
- main.go
package main
import (
"context"
"log"
"os"
// golang.org
"golang.org/x/oauth2/google"
// google.golang.org
"google.golang.org/api/option"
// cloud.google.com
"cloud.google.com/go/datastore"
)
func main() {
ctx := context.Background()
// 1. Get google application credentials.
var credOption option.ClientOption
credentials, err := google.FindDefaultCredentials(ctx)
if err != nil {
log.Fatal(err)
}
if credentials.JSON != nil {
credOption = option.WithCredentialsJSON(credentials.JSON)
} else {
credOption = option.WithTokenSource(credentials.TokenSource)
}
// 2. Put value.
err = PutValue("xxxxxxxxxxx", "example", credOption)
if err != nil {
log.Fatal(err)
}
}
type Item struct{
Key *datastore.Key `datastore:"__key__"`
Value string `datastore:"value"`
}
func PutValue(nameKey, value string, options ...option.ClientOption) error {
ctx := context.Background()
// 1. Create client.
client, err := datastore.NewClient(ctx, os.Getenv("DATASTORE_PROJECT_ID"), options...)
if err != nil {
return err
}
defer client.Close()
// 2. Put item.
_, err = client.Put(
ctx,
datastore.NameKey("Entity", nameKey, nil),
&Item{
Value: value,
},
)
if err != nil {
return err
}
return nil
}
- main_test.go
package main
import (
"net/http"
"testing"
// github.com
"github.com/jarcoal/httpmock"
"github.com/stretchr/testify/assert"
// google.golang.org
"google.golang.org/api/option"
)
func TestDatastore(t *testing.T) {
credOption := option.WithCredentialsJSON([]byte("hoge"))
t.Run("PutValue", func(t *testing.T) {
mockHTTPClient := &http.Client{}
httpmock.ActivateNonDefault(mockHTTPClient)
defer httpmock.Reset()
err := PutValue(
"xxxxxxxxxxx",
"example",
credOption,
option.WithHTTPClient(mockHTTPClient),
)
assert.Nil(t, err)
})
}
- test
$ go test -v ./...
--- FAIL: TestDatastore (0.00s)
--- FAIL: TestDatastore/PutValue (0.00s)
main_test.go:29:
Error Trace: main_test.go:29
Error: Expected nil, but got: &errors.errorString{s:"dialing: WithHTTPClient is incompatible with gRPC dial options"}
Test: TestDatastore/PutValue
FAIL
FAIL command-line-arguments 0.036s
ソース読んでみたところ
- ざっくり言うと
http.Client
で通信するわけじゃないと言うこと
Cloud Datastore をテストするには
1. main_test.go で httpmock を使うのをやめる
package main
import (
"testing"
// github.com
"github.com/stretchr/testify/assert"
// google.golang.org
"google.golang.org/api/option"
)
func TestDatastore(t *testing.T) {
credOption := option.WithCredentialsJSON([]byte("hoge"))
t.Run("PutValue", func(t *testing.T) {
err := PutValue(
"xxxxxxxxxxx",
"example",
credOption,
)
assert.Nil(t, err)
})
}
2. エミュレーターを起動する
$ docker run -d -it --rm --name cloud-datastore-emulator --entrypoint gcloud google/cloud-sdk:latest beta emulators datastore start --project=eg-example-01 --host-port=0.0.0.0:8081
3. go test
$ export DATASTORE_PROJECT_ID=eg-example-01
$ export DATASTORE_EMULATOR_HOST=datastore:8081
$ go test -v ./...
=== RUN TestDatastore
=== RUN TestDatastore/PutValue
--- PASS: TestDatastore (0.08s)
--- PASS: TestDatastore/PutValue (0.08s)
PASS
ok command-line-arguments 0.158s
GitLab CI 化
- .gitlab-ci.yml
image: docker:stable
stages:
- testing
variables:
GIT_SUBMODULE_STRATEGY: recursive
GO_VERSION: "1.12"
Test golang:
stage: testing
image: golang:${GO_VERSION}
variables:
DOCKERIZE_VERSION: v0.6.1
DATASTORE_EMULATOR_HOST: datastore:8081
DATASTORE_PROJECT_ID: eg-example-01
services:
- name: google/cloud-sdk:latest
alias: datastore
entrypoint: ["gcloud", "beta", "emulators", "datastore"]
command: ["start", "--project", "eg-example-01", "--host-port", "0.0.0.0:8081"]
before_script:
- curl -sfL https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz | tar -C /usr/local/bin -xzv
- dockerize -wait tcp://${DATASTORE_EMULATOR_HOST} -timeout 1m
script:
- go test -v ./...
- services に Cloud Datastore Emulator のコンテナを指定
- ローカルでテストするときは
--host-port=localhost:8081
とか指定してもいいけど、それだとコンテナ外部から通信できないので--host-port=0.0.0.0:8081
にする
- ローカルでテストするときは
- 環境変数に DATASTORE_PROJECT_ID, DATASTORE_PROJECT_ID を追加する
- DATASTORE_PROJECT_ID は services で指定したエイリアスをホスト名にする
- エミュレーターがちゃんと起動してからテストしたいので dockerize で待つ
- あとはテストするだけ