こんにちは。サーバサイドエンジニアの梅垣です。
アドベントカレンダー16日目を担当します。
記事の目的
コンテナで動いているDBを使ったユニットテストをGithub Actionsを実行する方法を説明します。
Github Actions は任意のリポジトリに.github/workflows/*.yml
ファイルを設置するだけでソフトウェア開発ワークフローを自動化してくれます。便利ですよね〜。
テスト対象のコードを準備
説明を簡単にするために、「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"` }
ちなみに環境変数を取得するのに使っているライブラリはこちらです。いろいろと使い勝手が良くお世話になっているので紹介しておきます。
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) } }
コンテナ上でサンプルアプリを動かす
サンプルアプリの実装ができたので、実際にアプリが動くか確認します。
なので、Dockerfile
とdocker-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
これまた余談ですが、ホットリロード機能に使っているツールはこちらです。
*.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していることを確認できます。
詳細を見るとユニットテストが実行されていることを確認できます。
. . . 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にご興味のある方、是非ご応募ください!