コンテナで動くDBとアプリとの疎通をGithub Actionsでテストする

こんにちは。サーバサイドエンジニアの梅垣です。

qiita.com

アドベントカレンダー16日目を担当します。

記事の目的

コンテナで動いているDBを使ったユニットテストをGithub Actionsを実行する方法を説明します。 Github Actions は任意のリポジトリに.github/workflows/*.ymlファイルを設置するだけでソフトウェア開発ワークフローを自動化してくれます。便利ですよね〜。

docs.github.com

f:id:hrb-umegaki-keisuke:20201216095043p:plain

テスト対象のコードを準備

説明を簡単にするために、「Postgresに対してping したら5秒スリープするを繰り返すアプリ」を開発しているとします。(めちゃくちゃシンプルですね☺️)

このアプリでは、ping が成功する限り無限ループするので「5秒スリープしてpingを繰り返す」という部分はテストすることが難しいです。なので、シンプルにping が成功することをテストで保証します。

環境変数を取得

環境変数からPostgresの設定情報を取得します。環境変数自体は、docker-compose.ymlで渡すように設定します。

package main

import (
    "github.com/kelseyhightower/envconfig"
)

var envVars Variables = initVariables()

func initVariables() Variables {
    var vars Variables
    if err := envconfig.Process("", &vars); err != nil {
        panic(err)
    }
    return vars
}

type Variables struct {
    PsqlPort     int    `envconfig:"PSQL_PORT"`
    PsqlHost     string `envconfig:"PSQL_HOST"`
    PsqlUser     string `envconfig:"PSQL_USER"`
    PsqlPassword string `envconfig:"PSQL_PASSWORD"`
    PsqlDatabase string `envconfig:"PSQL_DATABASE"`
    PsqlSslMode  string `envconfig:"PSQL_SSLMODE"`
}

ちなみに環境変数を取得するのに使っているライブラリはこちらです。いろいろと使い勝手が良くお世話になっているので紹介しておきます。

github.com

Postgresと疎通する

Go標準のライブラリのみを使いました。今回のサンプルアプリの一番重要な部分です。

下記の動作を実装しています。

  • 環境変数からPostgresの設定情報を取得
  • Postgresに接続する
  • PostgresにPingする
  • エラーが出ない限りPostgresへPingして5秒スリープを繰り返す
package main

import (
    "database/sql"
    "fmt"
    "time"

    _ "github.com/lib/pq"
)

func psqlDNS() string {
    return fmt.Sprintf(
        "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
        envVars.PsqlHost,
        envVars.PsqlPort,
        envVars.PsqlUser,
        envVars.PsqlPassword,
        envVars.PsqlDatabase,
        envVars.PsqlSslMode,
    )
}

type psqlClient struct{ *sql.DB }

// Postgresに接続する
func connPsql() *psqlClient {
    db, err := sql.Open("postgres", psqlDNS())
    if err != nil {
        panic(err)
    }
    return &psqlClient{db}
}

// PostgresへPingする
func (c psqlClient) ping() error {
    return c.DB.Ping()
}

// エラーが出ない限りPostgresへPingして5秒スリープを繰り返す
func (c psqlClient) watch() error {
    for {
        if err := c.ping(); err != nil {
            return err
        }
        fmt.Printf("It is success to ping: %+v\n", time.Now())
        time.Sleep(5 * time.Second)
    }
}

main() 関数を実装

アプリのエントリポイントとなるファイルです。 Postgresのコネクションを取得した後、「Postgresに対してping したら5秒スリープするを繰り返す」watch()関数を呼び出しています。

package main

func main() {
    client := connPsql()
    defer client.Close()
    if err := client.watch(); err != nil {
        panic(err)
    }
}

コンテナ上でサンプルアプリを動かす

サンプルアプリの実装ができたので、実際にアプリが動くか確認します。

なので、Dockerfiledocker-compose.yml を用意します。

./docker/golang/local/Dockerfile では下記を行っています。

  • サンプルコードをコンテナへコピー
  • ホットリロード機能を提供してくれるライブラリをインストール
    • air.toml に設定がある
  • コンテナ起動時にPostgresの起動を待ってからairコマンドでアプリを起動
FROM golang:1.15.5-alpine
RUN apk update && apk add git make gcc g++
WORKDIR /go/src/app
RUN go get -u github.com/cosmtrek/air
COPY go.mod .
COPY go.sum .
COPY . .
CMD while ! nc -z ${PSQL_HOST} ${PSQL_PORT}; do sleep 1; done && \
    air -c docker/golang/local/air.toml

これまた余談ですが、ホットリロード機能に使っているツールはこちらです。

github.com

*.toml ファイルに設定を書いてair -c path/to/*.toml とコマンドを実行するとホットリロードしてくれます。よかったら使ってみて下さい。

サンプルコードで使っているair.tomlは、air/air_example.toml at master · cosmtrek/air · GitHub を参考にしています。

air.tomlの中身

# Config file for [Air](https://github.com/cosmtrek/air) in TOML format

# Working directory
# . or absolute path, please note that the directories following must be under root.
root = "."
tmp_dir = "tmp"

[build]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o ./tmp/main"
# Binary file yields from `cmd`.
bin = "tmp/main"
# Customize binary.
full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
# Watch these filename extensions.
include_ext = ["go"]
# Ignore these filename extensions or directories.
exclude_dir = ["docker"]
# Watch these directories if you specified.
include_dir = []
# Exclude files.
exclude_file = []
# Exclude unchanged files.
exclude_unchanged = true
# This log file places in your tmp_dir.
log = "air.log"
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 1000 # ms
# Stop running old binary when build errors occur.
stop_on_error = true
# Send Interrupt signal before killing process (windows does not support this feature)
send_interrupt = false
# Delay after sending Interrupt signal
kill_delay = 500 # ms

[log]
# Show log time
time = false

[color]
# Customize each part's color. If no color found, use the raw app log.
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"

[misc]
# Delete tmp directory on exit
clean_on_exit = true

サンプルアプリとDBを起動するdocker-compose.yml です。

version: "3.8"
services:
  golang:
    container_name: app
    build:
      context: .
      dockerfile: ./docker/golang/local/Dockerfile
    volumes:
      - .:/go/src/app:delegated
    environment:
      PSQL_HOST: postgres
      PSQL_PORT: 5432
      PSQL_USER: psql_user
      PSQL_PASSWORD: psql_password
      PSQL_DATABASE: psql_database
      PSQL_SSLMODE: disable
    depends_on:
      - postgres

  postgres:
    image: postgres
    restart: always
    volumes:
      - .data/postgres:/var/lib/postgresql/data/pgdata:delegated
    environment:
      PGDATA: /var/lib/postgresql/data/pgdata
      POSTGRES_USER: psql_user
      POSTGRES_PASSWORD: psql_password
      POSTGRES_DB: psql_database

コンテナでアプリを起動する準備ができたので、実際に動かします。

  • Dockerfileからイメージを作成
  • ネットワークを作成
  • アプリを起動

さて、「Postgresにpingして5秒スリープを繰り返す 」ができているかな〜🤫 下記コマンドで実行してコンテナを起動します。

$ docker-compose build
$ docker-compose up -d --remove-orphans

ping して5秒スリープを繰り返すことに成功していますね!!💯

app         | 
app         |   __    _   ___  
app         |  / /\  | | | |_) 
app         | /_/--\ |_| |_| \_  // live reload for Go apps, with Go 
app         | 
app         | mkdir /go/src/app/tmp
app         | watching .
app         | !exclude docker
app         | !exclude tmp
app         | building...
app         | go: downloading github.com/kelseyhightower/envconfig v1.4.0
app         | go: downloading github.com/lib/pq v1.9.0
app         | running...
app         | It is success to ping: 2020-12-14 22:50:44.6123172 +0000 UTC m=+0.090423201
app         | It is success to ping: 2020-12-14 22:50:49.6143721 +0000 UTC m=+5.092481401
app         | It is success to ping: 2020-12-14 22:50:54.5818227 +0000 UTC m=+10.094412801

テストコードを書く

アプリが動いていることも確認したので、ping() 部分のテストを書きます。

package main

import (
    "testing"

    _ "github.com/lib/pq"
    "github.com/stretchr/testify/assert"
)

func Test_psqlClient_ping(t *testing.T) {
    tests := []struct {
        name       string
        psqlClient *psqlClient
        wantErr    string
    }{
        {
            name:       "success to ping",
            psqlClient: connPsql(),
            wantErr:    "",
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := tt.psqlClient.ping()
            if tt.wantErr != "" {
                assert.EqualError(t, err, tt.wantErr)
                return
            }
            assert.NoError(t, err)
        })
    }
}

これを実行してPASSしたら、サンプルアプリがPostgresに対してping することができることを保証できそうです。

このテストをローカルで(コンテナを使わないで)実行すると失敗します。 環境変数がホストに設定していないのでPostgresの設定情報が取得できず、ping先のホストが見つからずエラーになるからです。

--- FAIL: Test_psqlClient_ping (0.00s)
    --- FAIL: Test_psqlClient_ping/success_to_ping (0.00s)
        /Users/umegakikeisuke/go/src/github.com/kskumgk63/gotest-using-container-on-githubactions/psql_test.go:26: 
                Error Trace:    psql_test.go:26
                Error:          Error message not equal:
                                expected: ""
                                actual  : "dial tcp: lookup port=0: no such host"
                Test:           Test_psqlClient_ping/success_to_ping
FAIL
FAIL    github.com/kskumgk63/gotest-using-container-on-githubactions    0.392

コンテナ上でテストを実行する

コンテナを起動してコンテナの中でテストを行います。

$ docker-compose up -d --remove-orphans
$ docker-compose exec golang go test -cover ./...

成功しました👍

docker-compose exec golang go test -cover ./...
ok      github.com/kskumgk63/gotest-using-container-on-githubactions (cached)        coverage: 42.1% of statements

GithubActionsでテストを実行する

ローカル環境でコンテナを使ってテストを成功させることができました。では、Github Actinosを使ってテストを実行させます。

.github/workflows/test.yml を準備します。

このワークフローで行うことは、「コンテナ上でテストを実行する」で実行したコマンドをGithub Actions 上で実行するだけです。

name: test
on:
  pull_request:
    branches:
      - main
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Test with docker-compose stack
        run: |
          docker-compose up -d --remove-orphans
          docker exec app go test -cover -v ./...

Github Actionsを動かす

先程のtest.yml を書いてPullRequestを出すと、ワークフローが動いてテストがPASSしていることを確認できます。

f:id:hrb-umegaki-keisuke:20201215100717p:plain

f:id:hrb-umegaki-keisuke:20201215100809p:plain

詳細を見るとユニットテストが実行されていることを確認できます。

.
.
.
Image for service golang was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Successfully tagged gotest-using-container-on-githubactions_golang:latest
Creating gotest-using-container-on-githubactions_postgres_1 ... 

Creating gotest-using-container-on-githubactions_postgres_1 ... done
Creating app                                                ... 

Creating app                                                ... done
go: downloading github.com/lib/pq v1.9.0
go: downloading github.com/stretchr/testify v1.6.1
go: downloading github.com/kelseyhightower/envconfig v1.4.0
go: downloading gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
go: downloading github.com/pmezard/go-difflib v1.0.0
go: downloading github.com/davecgh/go-spew v1.1.0
=== RUN   Test_psqlClient_ping
=== RUN   Test_psqlClient_ping/success_to_ping
--- PASS: Test_psqlClient_ping (0.00s)
    --- PASS: Test_psqlClient_ping/success_to_ping (0.00s)
PASS
coverage: 42.1% of statements
ok      github.com/kskumgk63/gotest-using-container-on-githubactions    0.007s  coverage: 42.1% of statements

さいごに

今回は簡単なアプリを使ってコンテナのDBを使ったユニットテストをGithubActionsで、どう実行させるかを説明しました! コンテナやGithub Actionsを使ってガンガンテストを書いていきましょう!!

最後に今日使ったサンプルコードを載せておきます。

kskumgk63/gotest-using-container-on-githubactions

宣伝

HRBrainにご興味のある方、是非ご応募ください!

www.wantedly.com