Edited at

CircleCI Orbsをテストする

CircleCI Advent Calendar 2018の4日目です🎄

3日目の昨日は @tomonorimatsumura さんの「CircleCIでOrbsを利用して並列処理を行う」でした。


はじめに

先日、Shallow Cloneに対応したリポジトリのチェックアウトができるGit Orbを公開しました。

Orbを作成するにあたって、「テストはどのようにすればよいのか?」ということは関心事のひとつです。

この記事ではCircleCIのドキュメントにあるテストのレベルと、Git Orbで使っているOrb Toolsを中心に、情報を整理してみます。


テストのレベル

公式ドキュメントのTesting orbsによると、Orbのテストには複雑さとスコープによって上がっていく4つのレベルが示されています。



  1. Schema Validation: OrbがYAML形式として正しいか、Orbのスキーマに準拠しているかをチェックします。CLIのコマンド1つで実行できます。


  2. Expansion Testing: 開発しているOrbの呼び出しを含む設定に対し、インライン展開する処理を行って意図した設定が生成されるかをテストします。これはCircle CLIのコマンドを組み合わせて確認できます。


  3. Runtime Testing: テストごとに必要なセットアップを行い、CircleCIのビルド内でテストを実行します。


  4. Integration Testing: 外部のサービスと実際に連携してテストします。連携先のテスト用環境が必要となります。このレベルが必要になるケースはコンテキストごとに手段が大きく異なりそうであることと、私自身まだノウハウが溜まっていないため、本記事ではこのレベルについては触れません。

レベル1と2は静的なチェックになるため、多くのケースではレベル3のRuntime Testing以上が必要になってくるでしょう。


テストのレベルごとのCircleCI CLIを使った例

上記のテストのレベルごとに、CircleCI CLIでどのようにテストできるかの例をまとめてみました。

これらを把握することで、後述のOrb Tools内部でCircleCI CLIによって何が行われているかの理解にも役立ちます。


Schema Validation

CircleCI CLIのcircleci orb validateコマンドを実行することで、ファイルのチェックができます。

引数にYAMLファイルを渡します。

$ circleci orb validate ./src/orb.yml

標準入力からYAMLを受け取ることもできます。

$ cat ./src/orb.yml | circleci orb validate -


出力例


YAMLとしてパースできない場合


src/orb.yml

version: 2.1

description: This is sample.

commands:
hello:
description: Output 'Hello'
steps:
- run: echo 'Hello'
invalid_string



出力結果

$ circleci orb validate ./src/orb.yml

Error: Unable to parse YAML
while scanning a simple key
in 'string', line 9, column 1:
invalid_string
^
could not find expected ':'
in 'string', line 10, column 1:

^



Orbとして構文エラーがある場合


src/orb.yml

version: 2.1

description: This is sample.

commands:
hello:
description: Output 'Hello'
step:
- run: echo 'Hello'



出力結果

$ circleci orb validate ./src/orb.yml

Error: ERROR IN CONFIG FILE:
commands: hello: 0 subschemas matched instead of one
commands: hello: 2 schema violations found
commands: hello: required key [steps] not found
commands: hello: extraneous key [step] is not permitted
commands: hello: expected type: String, found: Mapping


Expansion Testing

CircleCI CLIのコマンドを組み合わせて、Orbをテスト用の設定ファイルにインライン展開し、その構造をチェックします。

次のOrbを例に説明します。


src/orb.yml

version: 2.1

executors:
default:
parameters:
tag:
type: string
default: "curl-browsers"
docker:
- image: circleci/buildpack-deps:<< parameters.tag >>

jobs:
hello-build:
executor: default
steps:
- run: echo "Hello, build!"


この段階でcircleci orb validateコマンドによるSchema Validationも行っておきましょう。もし問題があった場合は詳細なエラー結果を得ることができます。検証が通ったらdevelopmentバージョンとして公開します。

$ circleci orb publish src/orb.yml namespace/orb@dev:0.0.1

このOrbを利用するconfig.ymlを用意します。


circleci/config.yml

version: 2.1

orbs:
hello: namespace/orb@dev:0.0.1

workflows:
hello-workflow:
jobs:
- hello/hello-build


circleci config processコマンドをconfig.ymlに適用して、Orbをインライン展開させます。

$ circleci config process .circleci/config.yml

展開した結果は次のようになります:

version: 2

jobs:
hello/hello-build:
docker:
- image: circleci/buildpack-deps:curl-browsers
steps:
- run:
command: echo "Hello, build!"
workflows:
hello-workflow:
jobs:
- hello/hello-build
version: 2

# Original config.yml file:
# version: 2.1
#
# orbs:
# hello: namespace/orb@dev:0.0.1
#
# workflows:
# hello-workflow:
# jobs:
# - hello/hello-build

このレベルのテストでは、期待通りに値が展開されているかをチェックします。

例えば、Dockerイメージのタグがデフォルト値として指定したcurl-browsersになっているかを確認するために、yqコマンドを使って次のように取り出すことができます。

$ circleci config process .circleci/config.yml | yq read - 'jobs.hello/hello-build.docker[0].image'

circleci/buildpack-deps:curl-browsers

テストを自動化する場合は、この出力をアサーションすればよいでしょう。


Runtime Testing

このレベルではOrbを用いた設定ファイルをcircleci local executeコマンドを使って実行します。circleci local executeがDockerを利用しているため、CircleCIのジョブの中で実行する場合はmachine executorを用いる必要があります。

$ circleci local execute -c test/sample.yml | tee output.log

デフォルトは.circleci/config.ymlを参照しますが、-cオプションでファイル名を指定できるので、テストケースごとにファイルを作っておけます。teeコマンドも併用して出力結果を保存しておき、次のステップやpost-stepgrepコマンド等を用いて出力のアサーションを行うとよいでしょう。


注意点

circleci local executeコマンドの大きな注意点として、Workflowに対応していないことが挙げられます。つまり、そのままではOrbのJobのテストが行なえません。

そこで、buildという名前のジョブに置き換えることで実行できるようにします。

まず、Workflowに定義したジョブにnameパラメーターを使ってbuildという名前を付与します。

version: 2.1

orbs:
hello: namespace/orb@dev:0.0.1

workflows:
main:
jobs:
- hello/hello-build:
name: build

そして、circleci config processコマンドで処理すると、名前がbuildのジョブに置き換わります。

version: 2

jobs:
build:
docker:
- image: ...
steps:
- run:
...
workflows:
main:
jobs:
- build
version: 2

これでcircleci local executeで実行できるようになりました。Workflowの定義は残っていますが、buildという名前のジョブさえいれば無視されます。

※ この方法はCircleCI Orbs 入門 | tsub's blogを参考にさせていただきました。


Orb Toolsを使ったテスト

実際にCircleCIでOrbのテストを実行する場合はCircleCI CLIをラップしたOrb Toolsを使うのが便利です。

ここではGit Orbで行っているテストをベースに説明します。


orb-tools orbの呼び出しを追加

Orb自身の.circleci/config.ymlにorb-tools orbを使った自動テストの定義を追加していきます。


.circleci/config.yml

version: 2.1

orbs:
orb-tools: circleci/orb-tools@volatile



公開用に1つのYAMLへパッキング

Orbのソースコードは分割してOrbの構造に通りにファイル配置しておくと、circleci orb packコマンドで1つのYAMLファイルへパッキングできます。

このファイル配置はartifactory orbのソースコードを参照するとわかりやすいです。

次のようにファイルが配置されているとします。

src/

├── @orb.yml
├── commands
│   ├── command-1.yml
│   └── command-2.yml
├── executors
│   └── executor-1.yml
└── jobs
└── job-1.yml

src直下の@orb.ymlの中身は次のようになっていることとします。

version: 2.1

description: |
This is sample Orb.

この状態でcircleci config pack src/を実行すると次のYAMLが生成されます。(キーはアルファベット順にソートされます)

commands:

command-1:
# src/commands/command-1.ymlの中身
command-2:
# src/commands/command-2.ymlの中身
description: |
This is sample Orb.
executors:
executor-1:
# src/executors/executor-1.ymlの中身
jobs:
job-1:
# src/jobs/job-1.ymlの中身
version: 2.1

pack jobではCircleCIのジョブとして扱うのに便利なオプションを付けてcircleci orb packコマンドを実行することができます。


.circleci/config.yml

workflows:

version: 2
test-and-publish-to-development:
jobs:
- orb-tools/pack:
source-dir: src/
destination-orb-path: packed/orb.yml
workspace-path: packed/orb.yml
artifact-path: packed/orb.yml
validate: true

source-dirにOrbのソースコードが格納されているディレクトリを指定し、destination-orb-pathにパッキングした結果の出力先を指定します。

workspace-pathはWorkspaceに永続化するファイルを指定します。ここではパッキングしたOrbをWorkflowの次のジョブに渡すために使っています。

artifact-pathにはArtifactsに保存するファイルを指定します。パッキングしたOrbをしてしておくことで、ジョブのArtifactsから参照することができます。

Artifacts

validatetrueを設定しておくとcircleci orb validateコマンドによるチェックも実行してくれます。


テスト用の設定ファイルごとにテストを実行

test-in-builds jobとlocal-test-build commandを組み合わせて使うことで、あらかじめ用意したconfig.yml相当の設定ファイルと開発したOrbをパッキングし、Orbをインライン展開したものをcircleci local executeコマンドで実行してくれます。


.circleci/config

      - orb-tools/test-in-builds:

requires: [orb-tools/pack]
attach-workspace: true
orb-location: packed/orb.yml
orb-name: git
test-steps:
- orb-tools/local-test-build:
test-config-location: test/shallow-clone-checkout.yml
- orb-tools/local-test-build:
test-config-location: test/shallow-clone-checkout-with-https.yml

attachment-workspacetrueに設定して前のジョブでパッキングしたOrbを受け取り、orb-locationにそのパスを指定します。

orb-nameにはテスト用に用意したconfig.yml相当の設定ファイルからOrbを参照する名前を指定します。

テスト用設定ファイルは具体的には次のようになります:


test/shallow-clone-checkout.yml

version: 2.1

orbs:
cli: circleci/circleci-cli@volatile

jobs:
build:
executor: cli/default
working_directory: ~/test-shallow-clone-checkout
steps:
- git/shallow-clone-checkout


stepsに開発したOrbのcommandであるgit/shallow-clone-checkoutを指定していますが、Orb自体を呼び出す定義はこのファイルにはありません。orb-tools orbのlocal-test-build command内でcircleci config packコマンドとcircleci config processが実行され、Orbがインライン化されます。そしてインライン化された設定ファイルがcircleci local executeコマンドで実行されます。

local-test-build commandにはtest-config-locationにテスト用設定ファイルを指定する他に、should-failtrueを指定することでエラーとなるパターンのテストも行えます。また、external-check-stepsに追加で検証したいコマンドをステップとして指定することもできます。これはパラメーターが期待通り展開されているかを厳密にチェックしたいときに使えるでしょう。


developmentバージョンとして公開

テストが済んだらpublish jobを使ってdevlopmentバージョンを公開し、他のリポジトリ等から手動テスト可能な状態にしておきます。内部的にはcircleci orb publishコマンドが実行されます。


.circleci/config

      - orb-tools/publish:

requires: [orb-tools/test-in-builds]
orb-path: packed/orb.yml
orb-ref: "ganta/git@dev:${CIRCLE_BRANCH}-${CIRCLE_SHA1}"
publish-token-variable: "${ORB_PUBLISHING_TOKEN}"
validate: true
checkout: false
attach-workspace: true

orb-pathに公開したいOrbファイルのパスを、orb-refにdevelopmentバージョンとしてのリファレンス名を指定しています。タグをCircleCI内で参照できる環境変数を使って、dev:#{CIRCLE_BRANCH}-${CIRCLE_SHA1}のようにしておくとわかりやすいです。

publish-token-variableにはCircleCIにアクセスするためのトークンを指定します。User settingsのPersonal API Tokensからトークンを発行し、開発しているOrbのCircleCIプロジェクトのEnvironment VariablesにORB_PUBLISHING_TOKENという名前で登録しておくと、上記の記述で参照できるようになります。

Environment Variables

Workspace経由でOrbのコードを受け取って公開しており、このジョブでのリポジトリのチェックアウトは不要なためcheckoutfalseとしています。


最終的なconfig.yml

最終的に.circleci/config.ymlは次のようになります:


.circleci/config.yml

version: 2.1

orbs:
orb-tools: circleci/orb-tools@volatile

workflows:
version: 2
test-and-publish-to-development:
jobs:
- orb-tools/pack:
source-dir: src/
destination-orb-path: packed/orb.yml
workspace-path: packed/orb.yml
artifact-path: packed/orb.yml
validate: true

- orb-tools/test-in-builds:
requires: [orb-tools/pack]
attach-workspace: true
orb-location: packed/orb.yml
orb-name: git
test-steps:
- orb-tools/local-test-build:
test-config-location: test/shallow-clone-checkout.yml
- orb-tools/local-test-build:
test-config-location: test/shallow-clone-checkout-with-https.yml

- orb-tools/publish:
requires: [orb-tools/test-in-builds]
orb-path: packed/orb.yml
orb-ref: "ganta/git@dev:${CIRCLE_BRANCH}-${CIRCLE_SHA1}"
publish-token-variable: "${ORB_PUBLISHING_TOKEN}"
validate: true
checkout: false
attach-workspace: true


今回はOrb Tools自身のconfig.ymlを参考に書いてみたのですが、test-in-builds jobではExpansion Testingで行ったようにpublishしたdevelopmentバージョンのOrbを参照するのではなく、Orbのコードをそのままインライン化していた点がわかりづらかったです。

publish jobを先にしてdevelopmentバージョンを公開し、それを参照するテスト用の設定を用意し、test-in-builds jobを経由せずに直接local-test-build commandを呼び出した方が実際に利用される状態に近くなるため、テストとしてはよいかもしれません。


(参考) Batsを用いたテスト

BatsはBash向けのテスティングフレームワークです。Orbの確認に便利なアサーションを作成してテストの可読性を高めることができます。artifactory orbtests配下を覗いてみると面白いです。

アサーションに用いる関数をヘルパーにがんばって定義しているので、この辺を再利用できる仕組みができると便利になりそうです。

ただし、オリジナルのBatsは2013年以降積極的にメンテナンスが行われていないため、Bats-coreを用いたほうがよさそうです。


まとめ

まだまだ過渡期でテストの手法が固まっていませんが、逆にそれを確立していくところを体験できるのは楽しいですね。

現状ではやはり公式のOrb Toolsが一番お手軽だと思います。

明日のCircleCI Advent Calendar 5日目は @skzwksk さんです。