CircleCIでGo言語製のRest APIのテストを自動で回す〜docker-composeを利用して〜

## はじめに

こんにちは

コンテナを使ったアプリケーション開発もぼちぼち増えてきたように感じます。
その中でも開発環境において、複数のコンテナ・プロセスを管理したいためにdocker-composeを利用している方も多いのではないでしょうか。

今回は、開発環境で利用しているdocker-composeをそのまま利用して、
CircleCIで自動テストする一例をご紹介したいと思います。

## コンテナ構成

```yml:docker-compose.yml
version: '2'
services:
  db:
    build:
      context: db/
    expose:
      - "3306"
    environment:
      - MYSQL_ROOT_PASSWORD=root #rootパスワードの設定
      - MYSQL_DATABASE=test
      - MYSQL_USER=user
      - MYSQL_PASSWORD=password
    volumes:
      - ./db/mysql_data:/db/mysql_data
    ports:
      - "3306:3306"
  app:
    build:
      context: app/
    volumes:
      - ./app/:/go/src/app
    command: realize start --run --no-config # DEBUG
    restart: always
    ports:
      - "8080:8080"
    depends_on:
      - db
```

## 今回作成するAPI

GETだけできるとてもシンプルなものにします。

### Status Code: 200

リクエスト

```bash
GET /user/1
```

レスポンス

```json
{
    "name": "山田",
    "age": 11
}
```

### Status Code: 404

リクエスト

```bash
GET /user/3
```

レスポンス

```json
{
    "code": "Not Found",
    "message": "レコードが見つかりません"
}
```

### 対応するテーブル・スキーマ

```sql
desc users:
```

```sql
Field, Type, Null, Key, Extra
---------------------------------------
id , int(11), No, PRI, auto_increment
name, varchar(255), YES
age, int(11), YES
```

### 実装コード

```go:main.go
r.GET("/user/:id", func(c *gin.Context) {
		userId, _ := strconv.Atoi(c.Param("id"))
		db := getDB()
		var user model.User
		if err := db.Where("id = ?", userId).First(&user).Error; gorm.IsRecordNotFoundError(err) {
			c.JSON(http.StatusNotFound, gin.H{
				"code":    "Not Found",
				"message": "レコードが見つかりません",
			})
			return
		}
		c.JSON(http.StatusOK, user)
		defer db.Close()
	})
```

※Webフレームワークにgin, ORMにgormを採用しています。

### マッピングしているUser構造体

```go
type User struct {
	Id   int    `json:"-"`
	Name string `json:"name"`
	Age  int    `json:"age"`
}
```

### テストコード

200と404を確認します。

テストレコードの作成もテストコードの中でやってしまいます。
※headerのチェックのみの簡略的なものとします。

```go:main_test.go
func Test_Main(t *testing.T) {
	db, err := gorm.Open("mysql", "user:password@tcp(db:3306)/test?charset=utf8mb4&parseTime=True&loc=Local")
	if err != nil {
		panic(err.Error())
	}
	
	// make db scheme
	db.AutoMigrate(&model.User{})
	// insert test record
	db.Create(&model.User{Id: 1, Name: "山田", Age: 11})

	t.Run("Check HTTP 200", func(t *testing.T) {
		w := httptest.NewRecorder()
		req, _ := http.NewRequest(http.MethodGet, "/user/1", nil)
		req.Header.Set("Content-Type", "application/json")
		router.ServeHTTP(w, req)
		assert.Equal(t, http.StatusOK, w.Code)
	})

	t.Run("Check HTTP 404", func(t *testing.T) {
		w := httptest.NewRecorder()
		req, _ := http.NewRequest(http.MethodGet, "/user/2", nil)
		req.Header.Set("Content-Type", "application/json")
		router.ServeHTTP(w, req)
		assert.Equal(t, http.StatusNotFound, w.Code)
	})
}

```

## CircleCIのconfig.yml

ポイントは、
buildタイプにmachineビルドを選択し、imageにcircleci/classic:edgeを選択する点です。

```yml:.circleci/config.yml
version: 2
jobs:
  build:
    machine:
      image: circleci/classic:edge
    working_directory: ~/repo
    steps:
      - checkout
      - run:
          name: install docker-compose
          command: |
            curl -L https://github.com/docker/compose/releases/download/1.19.0/docker-compose-`uname -s`-`uname -m` > ~/docker-compose
            chmod +x ~/docker-compose
            sudo mv ~/docker-compose /usr/local/bin/docker-compose
      - run:
          name: docker-compose up
          command: |
            set -x
            docker-compose up -d
      - run:
          name: test
          command: docker-compose exec app go test -v main_test.go main.go
      - run:
          name: docker-compose down
          command: docker-compose down
```

#### 参考
- Choosing an Executor Type
    - https://circleci.com/docs/2.0/executor-types/#using-machine

- Installing and Using docker-compose
    - https://circleci.com/docs/2.0/docker-compose/?gclid=CjwKCAiA66_xBRBhEiwAhrMuLRGIavj7e-geV4Gb4pweZLAfQ_K0E3Mij6DjX9ZJNMQdnFd03PY8eBoC1HQQAvD_BwE#using-docker-compose-with-machine-executor

build結果

```bash
$ #!/bin/bash -eo pipefail
docker-compose exec app go test -v main_test.go main.go
^@^@[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /user/:id                 --> command-line-arguments.Router.func1 (3 handlers)
=== RUN   Test_Main
=== RUN   Test_Main/Check_HTTP_200
[GIN] 2020/01/25 - 07:51:33 | 200 |     2.23664ms |                 | GET      /user/1
=== RUN   Test_Main/Check_HTTP_404
[GIN] 2020/01/25 - 07:51:33 | 404 |    1.848185ms |                 | GET      /user/2
--- PASS: Test_Main (0.03s)
    --- PASS: Test_Main/Check_HTTP_200 (0.00s)
    --- PASS: Test_Main/Check_HTTP_404 (0.00s)
PASS
ok  	command-line-arguments	0.040s

```

細かいところはソースをご覧下さい。

- https://github.com/leica19/circleci-go-api

## 注意点

image: circleci/classic:edgeはローカル環境でpullすることができないため(2020/01/25時点)、ローカルでの動作確認はコンテナにアタッチして手動で行う必要があります。要は、

ローカルコマンド

```bash
$ circleci build
```

が必ずコケて使えないということです。
※おそらくCircleCI側の都合だと思います。#リソース制限など

## おわりに

Pull Requestの段階で強制的に自動テストが走るようにすることは、もはやチーム開発において必須の仕組みだと感じました。
これからもCircleCIなどのCIツールを活用して、アプリケーションの品質向上と将来的な開発速度の向上を目指したいと思います。
CircleCIを活用したおもしろい例があれば、ぜひご紹介下さい。