##はじめに
この記事はプログラミング初学者による備忘録用の記事であり、また、少しでも他の初学者のお役に立てればと思い書いています。
今回は、Laravel × GitHub × CircleCIを連携させた自動テストの実装を試みたので、その内容を備忘録としてまとめておきたいと思います。
間違いなどがございましたら、ご指摘のほどよろしくお願い致します
##自動テストの流れ
今回、備忘録として残す自動テストの流れは以下の図のようになります。
流れの説明
ローカルPCからGitHubへブランチをpushする際に、CircleCIがGitHubから対象となるコードを取得し、テストを実行するようになっています。
(自動テストの結果は、GitHub上で表示されるようになっています。)
テスト実行後、mainブランチにマージすることで一定の品質を保ったコードだけがmainブランチに取り込まれるようになります。
では、下記にて自動テストの構築手順などを説明したいと思います。
##1.GitHubとCircleCIを連携させる
まず最初に、GitHubとCircleCIを連携することから始めます。
ここでは4枚の画像を使って簡単に説明します。
CircleCIのログイン画面にアクセスすると、左端の画面が表示されるのでGitHubでログインを選択してください。
その後、左から2枚目の画像のようにアカウント情報の入力を求められるので入力してください。
入力後、左から3枚目の画像のようにSelect an organization画面に遷移し、先ほど入力したアカウントが表示されていると思うので、選択して次の画面に進みます。
最後は4枚目の画像のように、リポジトリの選択等を求められるので必要事項を入力後、Set up projectボタンを選択することで連携が完了します。
##2.config.ymlの編集
GitHubとCircleCIの連携が完了後、以下のようなデフォルトのconfig.yml
を編集します。
version: 2
jobs:
build:
docker:
# Specify the version you desire here
- image: circleci/php:7.3-node-browsers
... # 以下略
CircleCIでテストを実行するために、上記のconfig.yml
を以下の通りに編集します。
version: 2.1
jobs:
build:
docker:
- image: circleci/php:7.4-node-browsers
steps:
- checkout
- run: sudo composer self-update --1
- run: composer install -n --prefer-dist
- run: npm ci
- run: npm run dev
- run:
name: php test
command: vendor/bin/phpunit
###・編集内容の説明
・ version:
CircleCIのバージョンを指定します。
2, 2.0, 2.1のいずれかが使用可能であり、2.1
が多くの機能を利用できるので今回は2.1
を使用します。
・ jobs:(build:)
jobs:では、CircleCI処理の全体(workflow)の1つ以上のジョブを定義します。
ワークフローは1つ以上の一意の名前付きジョブで構成する必要があり、それらのジョブの定義をjobs
マップで行います。
今回は、build
というジョブを作成しています。
CircleCIのworkflowは下記のような感じです。
workflow
├── job1(今回はbuild:)
│ ├── step1
│ ├── step2
│ └── step3
├── job2
│ ├── step1
│ ├── step2
│ └── step3
└── job3
├── step1
├── step2
└── step3
``・ docker:`` **- image:(必須)** CircleCIの処理は、Dockerコンテナ上で処理するので、使用するDockerイメージを``image:``で指定します。 dockerに関するキーはimageの他に複数あるので[リンク先](https://circleci.com/docs/ja/2.0/configuration-reference/#docker)をご確認ください。
~キーの説明(一部)~
キー | 必須 | タイプ | 説明 |
---|---|---|---|
image | ○ | 文字列型 | 使用するカスタムDockerイメージの名前を指定する |
今回は、CircleCI製PHPイメージであるcircleci/php
を使用しています。
このcircleci/phpイメージ
のタグ付けのスキーム構成は以下の通りです。
circleci/php:<php-version>[-variant]
・php-version
使用する PHP のバージョンを指定します。 完全なセマンティック バージョニング形式でポイントリリースを指定するか (7.3.11 など)、またはマイナーリリースを指定できます (7.3 など)。 マイナーリリースを指定した場合は、将来的に PHPチームからパッチアップデートがリリースされた時点で、そのパッチアップデートを参照することになります。
・-variant
利用可能な場合は、バリアントタグもオプションとして使用できます。
例
・circle/php:7.4-node
Node.jsバリアントを使うことができます。
Node.js バリアントのベースは元のPHPイメージと同一ですが、こちらでは Node.js もインストールされます。
・circle/php:7.4-node-brower
ブラウザーバリアントのベースは元のPHPイメージと同一ですが、こちらではaptによりNode.js、Java、Selenium、ブラウザーの依存関係が事前インストールされます。
このイメージには、各ブラウザーおよびそのドライバーを使用するうえで必要なすべてのサポート対象ツールが含まれています。
・ steps:
steps:には、1つ以上の処理を定義します。
- checkout
checkoutは、GitHub
からCircleCI
の環境にコードをコピー(git clone)します。
設定済みのpath(デフォルトは working_directory)にソースコードをチェックアウトするために使用する特別なステップです。 コードのチェックアウトを簡便にすることを目的にしたヘルパー関数である、というのが特殊としている理由です。
HTTPS経由でgitを実行する場合はこのステップは使えません。ssh経由でチェックアウトするのと同じ設定を行う必要があります。
引用:checkout - Circle公式ドキュメント
- run: sudo composer self-update --1
composer self-update --1
を実行してcomposerを1系に更新します。
このステップを省くと、下記のようなエラーが起きる場合があるので注意してください。
composer2がリリースされたことが影響しているようです。
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover
In PackageManifest.php line 122:
Undefined index: name
Script @php artisan package:discover handling the post-autoload-dump event returned with error code 1
Exited with code exit status 1
CircleCI received exit code 1
- run: composer install -n --prefer-dist
Composerを使用して、PHP関連のパッケージインストールを行います。
CircleCIでテストを実施する際は、このように毎回パッケージをインストールする必要があります。
- run: npm ci
npm ciは、npmを使用してJavaScript
関連パッケージをインストールします。
~特徴~
・テストプラットフォーム、継続的インテグレーション、デプロイなどの自動化された環境で使用することを意図している
・既存のpackage-lock.json内の依存関係がpackage.json内の依存関係と一致しない場合、npm ciはpackage-lock.jsonを更新する代わりに、エラーで終了します。
・npm ciは1度にプロジェクト全体しかインストールできず、このコマンドで個別の依存関係を追加することはできません。
・node_modulesディレクトリが既に存在する場合は、npm ciがインストールを開始する前に自動的に削除されます。
また、npm ci
は以下のような場合に著しく高速になります。
・package-lock.jsonファイルがある場合
・node_modulesフォルダがないか、空である場合
- run: npm run dev
JavaScriptのトランスパイル
を行います。
- run:
name: php test
command: vendor/bin/phpunit
runでは、いくつかのキーを使用して処理の内容を定義します。
今回使用している、nameやcommandを記述するかどうかはユーザー次第です。
コマンドだけで処理の内容が明確であればnameでタイトル名をつける必要はないと思われます。
runは、あらゆるコマンドラインプログラムを呼び出すのに使います。設定値を表すマップを記述するか、簡略化した表記方法では、command や name として扱われる文字列を記述します。
引用:run - Circle公式ドキュメント
キーの説明(一部)
キー | 必須 | タイプ | 説明 |
---|---|---|---|
name | × | 文字列型 | CircleCI の UI に表示されるステップのタイトルを指定できます 省略すると、commandの内容がそのままステップのタイトルとして表示されます。 |
command | × | 文字列型 | シェルから実行するコマンド |
他にも、色々なキーが存在しますので詳細はリンク先をご覧下さい。 |
###・CircleCIでの環境変数
config.ymlの編集後、このままテストを実行しても環境変数APP_KEY
が存在しないことが原因でエラーが発生します。
~エラー発生の理由~
なぜ、環境変数APP_KEYが存在しないかというと、ローカルPCの開発環境に用意してある.envファイル
は、.gitignoreファイル
にてGit管理されないように設定しているのが大半であり、GitHubのリポジトリ上に.envファイルは存在しないからです。
GitHubのリポジトリ上に.envファイルが存在しないということは、GitHubのリポジトリからソースコードをコピーするCircleCIの実行環境上にも、.envファイルが存在しないということになるわけです。
結果、環境変数APP_KEYが存在しないことに繋がり、エラーが発生します。
~解決策~
下記のように、2つの解決策があります。
1.自動テスト用の環境変数ファイル(.env.testingファイル)を用意し、Git管理する
2.CircleCIの管理画面で、環境変数を設定する
今回は、解決策として1.自動テスト用の環境変数ファイルを用意し、Git管理するを選択します。
テスト用に用意するAPP_KEYを、本番環境などで使用しないテスト専用にします。
~テスト用の環境変数ファイルの作成~
開発環境上(今回の場合はLaravel)で、.envファイルをコピーし、.env.testingファイル
を作成することで下記のような構成にします。
.
└──laravel
└── .env
└── .env.testing
[重要]
※PHPUnitは、Laravelインストール後のデフォルトの設定で、.env.testingというファイルが存在すると、.envの代わりに環境変数ファイルとして使用するようになっています。
※.env.testingを作成すると、CircleCI上で実行するテストだけでなく、Docker環境上で実行するテストでも、.env.testingが使われるようになります。
~.env.testingの内容~
.envファイルをコピーし、下記のように編集します。
.env.testingファイル内のデータベース関連の環境変数をコメントアウトする理由は、PHPUnitの設定上phpunit.xml
ファイルの内容が優先されるので、使われないことを分かりやすくするためにコメントアウトしています。
APP_NAME=Laravel
APP_ENV=testing # この行をtestingに変更
APP_KEY=base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx= #テスト用のAPP_KEYの値を設定する
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
#MySQLデータベース関連の環境変数の先頭に#を付けコメントアウトする
#DB_CONNECTION=mysql
#DB_HOST=
#DB_PORT=
#DB_DATABASE=
#DB_USERNAME=
#DB_PASSWORD=
# 略
~テスト用のAPP_KEY作成方法~
ターミナルで下記のコマンドを順に実行します。(この手順は開発環境によって異なると思います。)
$ docker-compose exec app bash (コンテナに入る)
root@#######:/work# cd ディレクトリ指定
root@#######:/work/ディレクトリ名# php artisan key:generate --show
base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=
(実行後、この値をコピーしてAPP_KEYとします)
以上で、基礎的な構築は完了です。
###・テスト実行の確認
Docker環境上と実際にCircleCIでテストを実行してみます。
Docker環境上でテスト実行
下記のようにテストを実行して、実行結果でokが出ればテストは成功です。
$ docker-compose exec app bash
root@####:/work# cd ディレクトリ名
root@####:/work/ディレクトリ名# ./vendor/bin/phpunit tests/Feature
#実行結果
PHPUnit 9.5.2 by Sebastian Bergmann and contributors.
.............................................. 46 / 46 (100%)
Time: 00:03.063, Memory: 32.00 MB
OK (46 tests, 46 assertions)
CircleCIでテスト実行
GitHub上で、下のようなUIが表示されると成功です。
詳細はリンク先(CircleCI)で確認できるようになっています。
##3.テストを効率化する(キャッシュ化)
上記の手順で構築したテスト環境では、開発者が開発内容(コード)をGitHubにpushする度に、繰り返しテストが実行されるようになっています。
その際、テストの処理時間が短ければ短いほど開発の速度も向上するため、可能な限りテストの処理速度を上げたいものです。
CircleCIでは、テスト実行ごとに環境を構築してテスト終了後は構築した環境を破棄する仕組みとなっています。
環境構築に関する処理の中で、composer install
やnpm ci
に関しては、利用するパッケージが追加・更新されない限り毎回環境構築ごとに同じパッケージをインストールしていることになります。
そこで、CircleCI側のいくつかの処理結果をキャッシュ化して保存することでテスト実行時に使い回せるようにし、テスト実行の効率化を図りたいと思います。
###・PHP関連のパッケージをキャッシュ
まずは、composer install
コマンドによってインストールされる、PHP関連のパッケージをキャッシュするように修正します。
version: 2.1
jobs:
build:
docker:
- image: circleci/php:7.4-node-browsers
steps:
- checkout
- run: sudo composer self-update --1
#==========追加==========
- restore_cache:
key: composer-v1-{{ checksum "composer.lock" }}
#=======================
- run: composer install -n --prefer-dist
#==========追加==========
- save_cache:
key: composer-v1-{{ checksum "composer.lock" }}
paths:
- vendor
#=======================
- run: npm ci
- run: npm run dev
- run:
name: php test
command: vendor/bin/phpunit
~追加した内容の解説~
- restore_cache
restore_cacheでは、keyに設定されている内容を元に、保存されたキャッシュを復元します。
keyには、復元するキャッシュの名前を指定します。
キャッシュの復元が成功した場合、- run: composer install -n --prefer-dist
のステップではPHP関連のパッケージのインストール処理は行われません。
※restore_cacheを使用する際は、予めsave_cacheステップを利用してこのkeyに該当するキャッシュを保存しておく必要があります。
restore_cacheが成功した場合
restore_cacheが成功した場合、CircleCIでは下記のような結果が表示されます。
Found a cache from build 458 at composer-v1-#########################=
Size: 8.8 MiB
Cached paths:
* /home/circleci/project/backend/vendor
Downloading cache archive...
Validating cache...
Unarchiving cache...
キーの説明
キー | 必須 | タイプ | 説明 |
---|---|---|---|
key | ○ | 文字列型 | 復元するキャッシュキーを1つだけ指定します |
keys | ○ | リスト | 復元するキャッシュを検索するためのキャッシュキーのリスト 最初にマッチしたキーのみが復元される |
name | × | 文字列型 | CircleCIのUIに表示されるステップ名を指定する |
※少なくとも1つのkeyを指定する必要があります。 | |||
keyとkeysの両方が指定されたときは、keyの内容がまず始めに検証され、次にkeysの内容が検証されます。 |
**- save_cache** [save_cache](https://circleci.com/docs/ja/2.0/configuration-reference/#savecache)では、**keyに指定した一意の名前でCircleCIのオブジェクトストレージにある依存関係やソースコードのようなファイル、ディレクトリのキャッシュを生成し、保存します。** 保存するディレクトリ名やファイル名はpathsに指定する必要があり、今回はComposerによってPHP関連のパッケージがインストールされるディレクトリであるvendorを指定しています。
また、キャッシュを新たに保存するときは、特殊なテンプレートを含む形でkeyの値を指定することも可能です。
キーの説明
キー | 必須 | タイプ | 説明 |
---|---|---|---|
key | ○ | 文字列型 | キャッシュを識別するために一意の名前を指定します。 今回は、Composerによってインストールしたパッケージのキャッシュであることがわかるようcomposerと、キャッシュの変更に対応するためにversiron1の略語v1を組み合わせた名前を付けています |
paths | ○ | リスト | キャッシュに追加するディレクトリのリストを指定 |
~重要~
paths:では、指定したディレクトリやファイルをキャッシュとして保存します。
restore_cacheステップでキャッシュが見つからなかった場合は、composer installステップでvendorディレクトリが作成され、そこにPHP関連のパッケージがインストールされます。
そして、save_cacheステップでは、そのvendorディレクトリをキャッシュとして保存します。
※save_cacheステップのkeyで指定した名前と同じキャッシュが既に存在する場合、つまりrestore_cacheステップでキャッシュが見つかった場合はキャッシュの上書き保存などは行われません。
テンプレートの説明(一部)
テンプレート | 説明 |
---|---|
{{ checksum "filename" }} | ・{{ checksum "ファイル名" }}とすることで、ファイルをハッシュ化した値を算出しています。 ・このファイルはリポジトリでコミットしたものであり、かつ現在の作業ディレクトリからの絶対・相対パスで指定する必要があります。 ・restore_cache と save_cache の間でこのファイルが変化しないのが重要なポイントです。ファイル内容が変化すると、restore_cache のタイミングで使われるファイルとは異なるキャッシュキーを元にしてキャッシュを保存するためです。 |
~補足~
{{ checksum "composer.lock" }}
で指定したcomposer.lockでは、Composerによってインストールされた各パッケージのバージョンが、依存パッケージも含め管理されており、composer.lockファイルをハッシュ化した値をキャッシュのkey(一意の名前)に含めることで、
もし仮にcomposer.lockに変更があった場合でもハッシュ化した値もしっかり変更され、キャッシュのkey名も変更されるようになります。
その結果、restore_cache
では保存済みのキャッシュ(vendorディレクトリ)が復元されず、composer instalでvendorディレクトリが作成されるとともに新たにPHP関連のパッケージがインストールされます。
要するに、composer.lockに変更が無い限り、restore_chacheでは「前回のCircleCI実行時のsave_cacheで保存されたキャッシュ」を復元し、composer.lockに変更があれば、restore_chacheではキャッシュを復元せず、save_cacheで新しいkeyにてキャッシュを保存し直すということです。
save_cacheが成功した場合
save_cacheが成功した場合、CircleCIでは下記のような結果が表示されます。
Skipping cache generation, cache already exists for key: composer-v1-###=
Found one created at 2022-02-06 14:09:36 +0000 UTC
###・JavaScript関連のパッケージをキャッシュ
JavaScript関連パッケージをインストールするnpm ci
の結果をキャッシュするように編集します。
version: 2.1
jobs:
build:
docker:
- image: circleci/php:7.4-node-browsers
steps:
- checkout
- run: sudo composer self-update --1
- restore_cache:
key: composer-v1-{{ checksum "composer.lock" }}
- run: composer install -n --prefer-dist
- save_cache:
key: composer-v1-{{ checksum "composer.lock" }}
paths:
- vendor
#==========追加==========
- restore_cache:
key: npm-v1-{{ checksum "package-lock.json" }}
#=======================
- run: npm ci #==========この1行は削除==========
#==========追加==========
- run:
name: npm ci
command: |
if [ ! -d node_modules ]; then
npm ci
fi
- save_cache:
key: npm-v1-{{ checksum "package-lock.json" }}
paths:
- node_modules
#=======================
- run: npm run dev
- run:
name: php test
command: vendor/bin/phpunit
~追加した内容の解説~
PHP関連パッケージをキャッシュ化した時と同様に、restore_cacheで保存されたキャッシュを復元し、save_cacheでpathsに指定したディレクトリ等をキャッシュに保存していきます。
- restore_cache:
key:
では、npmでインストールしたパッケージのキャッシュであることが分かるようにnpmと、キャッシュの変更に対応するためにversiron1の略語v1を組み合わせたnpm-v1
を付けています。
テンプレートを使用した{{ checksum "package-lock.json" }}
では、package-lock.jsonファイルを指定します。
このpackage-lock.jsonでは、npmによってインストールされた各パッケージのバージョンが、依存パッケージも含め管理されています。
※キャッシュ管理の仕組みは、PHP関連パッケージのキャッシュ化と同様です。
- save_cache:
paths:
では、node_modulesディレクトリを保存します。
restore_cacheステップでキャッシュが見つからなかった場合は、npm ciステップでJavaScript関連パッケージをインストールします。
そして、save_cacheステップでは、そのpackage-lock.json
をテンプレートの{{ checksum "filename" }}
で指定してキャッシュとして保存します。
- シェルスクリプトのif文(条件式)
何故、条件式を書いたかというと、npm ci
コマンドはパッケージをインストールする前に一度node_modulesディレクトリを削除してしまうからです。
条件式を書かずにnpm ci
を実行すると、restore_cacheでnode_modulesをキャッシュから復元しても、直後にnpm ci
で削除されてしまい、キャッシュが無駄になってしまいます。
- run:
name: npm ci
command: |
if [ ! -d node_modules ]; then
npm ci
fi
#===構造の説明===
if 条件式; then
条件式がtrueの時に実行する処理
fi
~各コマンドの説明~
CircleCIのcommandに複数行に渡ってコマンドを記述する時は、まず最初に|
を記述し、次の行からコマンドを記述します。
コマンド | 説明 |
---|---|
if ~ fi | if文はifで開始してfiで終わらせます |
[] | []は必須であり、[の直後と、]の直前には半角スペースが必要となります。 |
-d node_modules | node_modulesがディレクトリならばtrueとなります。 |
!(NOT条件) | NOT条件の場合、trueとなります。そのため、[ ! -d node_modules ]は、node_modulesというディレクトリが存在すればfalseを、存在しなければtrueを返しnpm ciを実行します。 |
キャッシュが利用された場合、下のようにnpm ciステップ
が0s未満で済むようになります。
##テスト失敗時にmainブランチへのマージを不可にする
GitHubでのテスト対象となるリポジトリ画面で、SettingsタブからBranchesメニューを選択してください。
選択後、下の画像のようにmainブランチに対するルールを設定します。
~各項目の説明~
・Branch name pattern
保護するブランチ名のパターンを入力します。
今回はmainブランチへのマージに対する保護ルールを設定するので、mainと入力します。
・Require status checks to pass before merging
マージ前に何かしらのチェックを必要とするかどうかを設定します。
このチェックボックスにチェックを入れると、検索欄が表示され予測変換でci/circleci: buildと表示されます。
このCircleCIのbuildジョブが正常終了していることをマージ可否の条件として使用するので選択します。
・Include administrators
作成したルールをリポジトリの管理者にも適用するかどうかを設定します。
チェックをしない場合、テストが失敗してもこのリポジトリの管理者のGitHubアカウントを使えばmainブランチへのマージが出来てしまいます。
それを阻止するためにチェックを入れておきます。
###・ルールが適用されているかの確認
編集後、テストを実行しGitHubのプルリクエストの画面でci/circleci: buildの欄
を確認してください。
テストが成功すると、Requiredと表示されていると思います。
これは、このbuildジョブの正常終了が、マージするにあたって必須の条件となっていることを意味します。このRequiredがあればルールがきちんと適用されているます。
##まとめ
以上で、自動テストの環境構築は終了です。
今回のテストでは私自身の経験不足もありSQLiteを使用していますが、テストの品質を向上させる場合は開発環境と同じデータベースを使用する方が良いと思います。
リンク先でMySQLやPostgreSQLを使ったconfig.ymlの例がありますので参考にして下さい。