GitLab CI で Cloud Datastore のテストをする

### TL;DR

- golang でのテストのお話
- Cloud Datastore は gRPC なので `http.Client` の変更でモック化する [httpmock](https://godoc.org/github.com/thingful/httpmock) とかが使えない
- Cloud Datastore Emulator を GitLab CI の services として起動しておけばテストできる

### ことの発端

以下のようなプログラムがあったとして、 httpmock を利用して Cloud Datastore へのリクエストをモック化してテストコードを書こうとしたら datastore.Client の作成でエラーが出た．

- main.go

```golang
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

```golang
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

```bash
$ 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` で通信するわけじゃないと言うこと
    - https://github.com/googleapis/google-cloud-go/blob/master/datastore/datastore.go#L114
    - https://github.com/googleapis/google-api-go-client/blob/master/internal/settings.go#L86
    - https://github.com/googleapis/google-api-go-client/blob/master/transport/grpc/dial.go#L68

### Cloud Datastore をテストするには

- [Cloud Datastore Emulator](https://cloud.google.com/datastore/docs/tools/datastore-emulator) を使う

#### 1. main_test.go で httpmock を使うのをやめる

```golang
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. エミュレーターを起動する

```bash
$ 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

```bash
$ 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

```yaml
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 ./...
```

1. services に Cloud Datastore Emulator のコンテナを指定
    - ローカルでテストするときは `--host-port=localhost:8081` とか指定してもいいけど、それだとコンテナ外部から通信できないので `--host-port=0.0.0.0:8081` にする
2. 環境変数に **DATASTORE_PROJECT_ID**, **DATASTORE_PROJECT_ID** を追加する
    - **DATASTORE_PROJECT_ID** は *services* で指定したエイリアスをホスト名にする
3. エミュレーターがちゃんと起動してからテストしたいので dockerize で待つ
    - https://github.com/gcpug/nouhau/issues/8 を参考にしました
4. あとはテストするだけ