GitLab CIでMR作成時にREST APIテストを自動で実行する
はじめに
こんにちは、SHIFTの開発部門に所属している Katayama です。
今回はREST APIの自動テストをCI(GitLab CI)で実行するための設定について、GitHub ActionsやCircleCIとの違いにも触れつつ、みていきたいと思う。また、GitLab CIでテストを自動化するにあたり、トラブルが発生したがそれの解決方法にも触れたいと思う。
※テストというとモックの話が出てくるが、今回のテスト対象のAPIはデータベースへの接続の部分もモック化しないパターンでテストを行う。実際にクライアントからAPIを呼び出すことで、実際に本番環境でAPIが呼び出されるような状況下でテストを行うことを目的としているため(場合によっては、モックを作成して自動テストを流すという事も選択肢としてはあるだろう)。
CIで自動テストを実行できるようにするまでに必要だったこと
今回、GitLab CIで自動テストを実行できるようにするにあたり、以下のような対応が必要だった(一部はプロジェクトの設定に依存する部分だが、サービスが依存するデータベースサーバーを用意したり、初期データを投入するなどは一般的にあり得ることだと思う)。
依存するデータベース(MySQL、Redis)の起動
設定ファイルの変更(データベースの接続先の編集)
スキーマの構築と初期データの投入
サーバー起動までの待機処理の設定
上記のそれぞれについて、以下の章で詳細を見ていく。
依存するデータベース(MySQL、Redis)の起動
基本的にアプリケーションは何等かのデータベースに依存した形で実装されると思うが、今回自動テストを行おうとしていたアプリケーションもMySQL・Redisに依存していた。APIテストの自動化にあたってはそれらのデータベースも必要になるが、それはどうやって用意するのか?という問題がある(ローカルの開発環境では、docker-composeを利用してそれぞれを立ち上げていた)。
そこでGitLab CIに備わっているサービスという機能を利用する。このサービスを利用すると、runnerのimageから構築されるコンテナからアクセスできる別のコンテナをDockerイメージから構築してくれるようになり、例えば "mysql:8.0.34" のイメージをサービスに指定した場合は、"mysql" というホスト名でアクセスできるようになる。
これにより、CIのパイプラインの中であってもアプリケーションに依存するデータベースなどを立ち上げる事ができる。
今回はこのサービスを利用してパイプラインを実装した(commandの意味については、「おまけ」の「トラブルシューティング」の章を参照)。
# .gitlab-ci.yml
...
services:
- redis:6.2.5-alpine3.14
- name: mysql:8.0.34
command: [ "--default-authentication-plugin=mysql_native_password" ]
...
設定ファイルの変更(データベースの接続先の編集)
上記で見たように、GitLab CI サービスを利用して、データベースのコンテナを起動することで、CI環境でテストを実行できるようにした。
ただ、ローカル環境ではdocker-composeを利用してそれらのデータベースを立てて開発をしていたので、データベースへの接続先のホストが "localhost" になっていた。
そこで、そのホスト名をそれぞれ "mysql" や "redis" に変更する必要が出てきた(サービスへのアクセスに書かれている通り、サービスで立ち上げたコンテナへはサービス名でアクセスできる)。
具体的には、データベースの接続情報などはconfigを利用して、以下のように定義していた。
// config/default.json
{
...
"sequelize": {
...
"username": "root",
"password": "",
"host": "localhost",
"dialect": "mysql",
...
}
},
"redis": {
"session": {
"port": 6379,
"host": "localhost"
}
},
...
}
それをシェル上からファイル編集を行い、以下のように変更する処理を "before_script" に設定した。JSONの値を書き変えるシェルスクリプトは以下のようになった。
# .gitlab-ci.yml
before_script:
- ...
- jq '.sequelize.host = "mysql"' config/default.json > temp.json && mv temp.json config/default.json
- jq '.redis.session.host = "redis"' config/default.json > temp.json && mv temp.json config/default.json
- ...
この編集により、CI環境でもアプリケーションからデータベースに接続できる状態にすることができた。
※CIのrunnerは "image: node:16-alpine3.18" を利用していたので、jqはデフォルトではインストールされていなかった。そのため、"apk add jq" のようにして自前でインストールが必要。
スキーマの構築と初期データの投入
MySQLをサービスで立ち上げられてはいるが、空っぽ状態なのでスキーマの構築と必要に応じて初期データを投入する必要があるだろう。それは以下のようなシェルスクリプトで実現できるだろう。
# .gitlab-ci.yml.
before_script:
- ...
- mysql -h mysql -u root < sql/schema.sql
- mysql -h mysql -u root < sql/master_local.sql
- ...
※前提として、環境変数を以下のように設定していたので、MySQLへはパスワードなしでアクセスできる状態になっているものとする。
variables:
MYSQL_ROOT_PASSWORD: ''
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
TZ: 'Asia/Tokyo'
サーバー起動までの待機処理の設定
APIテストの自動化対象のアプリケーションはExpressのサーバーだったが、基本的にどんなサーバーでも起動しきるまでに若干リードタイムが発生する。サーバーが起動しきる前にテストが実行されるともちろんエラーになるので、サーバーが起動しきるまで待機する、という処理が必要になる事は多いと思う。
今回も待機処理が必要になったが、sleepのような処理では状況によってテストが失敗するというリスクやsleep時間の調整などがあると思うので、curlのretryで待機処理を実装した。スクリプトとしては以下。
curl --retry 10 --retry-connrefused --retry-delay 5 --retry-max-time 60 http://localhost:3000/healthcheck
スクリプトについて少し補足する。
--retry
リトライする最大回数--retry-connrefused
"connection refused" を一過性のエラーと見なす--retry-delay
リトライの間隔を5秒間隔に--retry-max-time
リトライ処理を行う最大の経過時間を60秒にする(60秒以上になるとリトライ処理が最大リトライ回数の10回に到達していなくてもコマンドが終了する)
上記のようなシェルスクリプトをテストの前に挟むことで、以下のようにサーバーの起動までを待機できる。
$ curl --retry 10 --retry-connrefused --retry-delay 5 --retry-max-time 60 http://localhost:3000/logout/client/uri
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
curl: (7) Failed to connect to localhost port 3000 after 7 ms: Couldn't connect to server <-- ここが待機の処理のログ
Warning: Problem : connection refused. Will retry in 5 seconds. 10 retries
Warning: left.
yarn run v1.22.19
$ yarn express:run
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
curl: (7) Failed to connect to localhost port 3000 after 1 ms: Couldn't connect to server <-- ここが待機の処理のログ
Warning: Problem : connection refused. Will retry in 5 seconds. 9 retries left.
DONE Wed Sep 27 2023 11:12:37 GMT+0900 (Japan Standard Time)
♻️ Server running at:
- Local: http://localhost:3000/
- Network: http://172.17.0.4:3000/
100 48 100 48 {"healthcheck":"ok"}0 0 60 0 --:--:-- --:--:-- --:--:-- 60
最終的に出来上がったパイプライン
上記で見てきたような設定を行い作成したパイプラインは以下のようになった。
# .gitlab-ci.yml
test_mr:
only:
- merge_requests
image: node:16-alpine3.18
services:
- redis:6.2.5-alpine3.14
- name: mysql:8.0.34
command: [ "--default-authentication-plugin=mysql_native_password" ]
variables:
MYSQL_ROOT_PASSWORD: ''
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
TZ: 'Asia/Tokyo'
stage: test
before_script:
- apk update
- apk add jq curl mysql mysql-client
- yarn install --frozen-lockfile
- jq '.sequelize.host = "mysql"' config/default.json > temp.json && mv temp.json config/default.json
- jq '.redis.session.host = "redis"' config/default.json > temp.json && mv temp.json config/default.json
- mysql -h mysql -u root < sql/schema.sql
- mysql -h mysql -u root < sql/master_local.sql
- yarn express:run &
- curl --retry 10 --retry-connrefused --retry-delay 5 --retry-max-time 60 http://localhost:3000/logout/client/uri
script:
- yarn test
こうして眺めてみると、GitHub ActionsやCircleCIとは結構違うなという印象を持つかもしれない。少し違いや共通点について整理してみると、以下のような違いがあるだろう。
違い
stage
GitLab CIには "stage" という概念がある。これが多段のパイプラインを実現するためのキーとなる機能だが、GitHub Actionsの場合は特にこうした概念はなく、単にjobがあってそれの依存関係は "needs" というキーワードで設定する。このあたりは大きな違いに感じた。GitHub Actionsではjobをいくつも定義してそれを "needs" で簡単に前後関係を設定できるが、GitLab CIだとまず stage を定義してあげる必要があり、ひと手間あるなと感じた。before_script, script
GitHub ActionsやCircleCIにはないと思うが、GitLab CIでは分けて設定できるようになっている模様。正直、yaml上での意味はあるが、パイプラインの処理としては "before_script" の実行内容は "script" に引き継がれる(同じシェル上で実行される)のであまり分かれている意味は感じられなかった。
共通点
サービス
GitLab CIでもGitHub Actionsでも、CI内でアプリケーションが依存するようなサービス(データベースなど)を立ち上げることができる、というのは全く同じ。E2Eのようなテストをするためには絶対に不可欠なのでそれぞれのCI/CDサービスでちゃんと機能が提供されているのだろうと感じた。
※ちなみに、複数のstageがあるパイプラインの定義配下のようにできる。
# .gitlab-ci.yml
build_mr:
only:
- merge_requests
image: node:16-alpine3.18
stage: build
before_script:
- yarn install
script:
- yarn build
test_mr:
only:
- merge_requests
image: node:16-alpine3.18
...
stage: test
before_script:
- ...
script:
- ...
こうすると、build -> test の順番にパイプラインが流れる(buildで失敗するとtestは実行されない)。そしてWebの画面上だと以下のような見た目になる。
まとめとして
今回はGitLab CIでAPIテストを自動化するための設定をやってみた。また、GitHub ActionsやCircleCIとの違いについても触れてみた。なかなか複数のCI/CDサービスを利用するという事はないと思うが、諸事情でVCSが分かれていたりする際にはそれぞれのサービスに対する理解が必要になるが、微妙に違いがあるので一筋縄ではいかないなと感じた。
最近ではプラットフォームエンジニアリングという話も出てきており、こうした部分をぽちっとなでできる世界が早く来てほしいなとも思った。
おまけ
トラブルシューティング
Plugin caching_sha2_password could not be loaded: Error loading shared library /usr/lib/mariadb/plugin/caching_sha2_password.so: No such file or directory
調べてみると既に同じことに困っている人がいたようでGitLab Forumに同じ内容の質問があった。
そこに書かれている方法の内、今回はサービスのコマンド設定という機能で、Dockerコンテナの起動時に以下のようなコマンドを渡して、MySQLの認証プラグインを変更して対応した。
services:
- name: mysql:8.0.34
command: [ "--default-authentication-plugin=mysql_native_password" ]
《この公式ブロガーの記事一覧》
《お問合せはお気軽に》
SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/
SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/
SHIFTの導入事例
https://service.shiftinc.jp/case/
お役立ち資料はこちら
https://service.shiftinc.jp/resources/
SHIFTの採用情報はこちら
PHOTO:UnsplashのPankaj Patel