CircleCI Orbsをテストする

CircleCI Advent Calendar 2018の4日目です🎄

3日目の昨日は @tomonorimatsumura さんの「[CircleCIでOrbsを利用して並列処理を行う](https://qiita.com/tomonorimatsumura/items/bb6ed741ee4807f88a18)」でした。

# はじめに

先日、Shallow Cloneに対応したリポジトリのチェックアウトができる[Git Orb](https://circleci.com/orbs/registry/orb/ganta/git)を公開しました。

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

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


# テストのレベル

公式ドキュメントの[Testing orbs](https://github.com/CircleCI-Public/config-preview-sdk/blob/8e6001785fe7a05c3c2941b8d1daf416ac114bc6/docs/orbs-testing.md)によると、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ファイルを渡します。

```shell-session
$ circleci orb validate ./src/orb.yml
```

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

```shell-session
$ cat ./src/orb.yml | circleci orb validate -
```

### 出力例

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

```yaml:src/orb.yml
version: 2.1
description: This is sample.

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

```shell-session:出力結果
$ 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として構文エラーがある場合

```yaml:src/orb.yml
version: 2.1
description: This is sample.

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

```shell-session:出力結果
$ 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を例に説明します。

```yaml: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バージョンとして公開します。

```shell-session
$ 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をインライン展開させます。

```shell-session
$ circleci config process .circleci/config.yml
```

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

```yaml
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](https://github.com/kislyuk/yq)コマンドを使って次のように取り出すことができます。

```shell-session
$ 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を用いる必要があります。

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

デフォルトは`.circleci/config.yml`を参照しますが、`-c`オプションでファイル名を指定できるので、テストケースごとにファイルを作っておけます。`tee`コマンドも併用して出力結果を保存しておき、次のステップや[`post-step`](https://github.com/CircleCI-Public/config-preview-sdk/blob/8e6001785fe7a05c3c2941b8d1daf416ac114bc6/docs/pre-and-post-steps.md)で`grep`コマンド等を用いて出力のアサーションを行うとよいでしょう。

### 注意点

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

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

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

```yaml
version: 2.1

orbs:
  hello: namespace/orb@dev:0.0.1

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

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

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

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

※ この方法は[CircleCI Orbs 入門 | tsub's blog](https://blog.tsub.me/post/introducing-to-circleci-orbs/)を参考にさせていただきました。


# Orb Toolsを使ったテスト

実際にCircleCIでOrbのテストを実行する場合はCircleCI CLIをラップした[Orb Tools](https://github.com/CircleCI-Public/orb-tools-orb)を使うのが便利です。

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


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

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

```yaml:.circleci/config.yml
version: 2.1

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


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

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

このファイル配置は[artifactory orbのソースコード](https://github.com/CircleCI-Public/artifactory-orb/tree/dcd42ba895a2a546b497d4d1dd2e6236875bbd52/src)を参照するとわかりやすいです。

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

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

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

```yaml
version: 2.1

description: |
  This is sample Orb.
```

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

```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`コマンドを実行することができます。


```yaml:.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](https://qiita-image-store.s3.amazonaws.com/0/7055/4abae8ae-58f1-40bf-8445-17a5d3228575.png)

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


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

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

```yaml:.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-workspace`を`true`に設定して前のジョブでパッキングしたOrbを受け取り、`orb-location`にそのパスを指定します。
`orb-name`にはテスト用に用意した`config.yml`相当の設定ファイルからOrbを参照する名前を指定します。

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

```yaml: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-fail`に`true`を指定することでエラーとなるパターンのテストも行えます。また、`external-check-steps`に追加で検証したいコマンドをステップとして指定することもできます。これはパラメーターが期待通り展開されているかを厳密にチェックしたいときに使えるでしょう。


## developmentバージョンとして公開

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

```yaml:.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](https://circleci.com/account/api)からトークンを発行し、開発しているOrbのCircleCIプロジェクトのEnvironment Variablesに`ORB_PUBLISHING_TOKEN`という名前で登録しておくと、上記の記述で参照できるようになります。

![Environment Variables](https://qiita-image-store.s3.amazonaws.com/0/7055/6522952d-2a6e-7ebd-321d-7ab36fcdcf3b.png)


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

## 最終的なconfig.yml

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

```yaml:.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](https://github.com/CircleCI-Public/orb-tools-orb/blob/007d290b91f9afb510e264992603a4038f4beced/.circleci/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](https://github.com/sstephenson/bats)はBash向けのテスティングフレームワークです。Orbの確認に便利なアサーションを作成してテストの可読性を高めることができます。[artifactory orb](https://github.com/CircleCI-Public/artifactory-orb)の[tests](https://github.com/CircleCI-Public/artifactory-orb/tree/dcd42ba895a2a546b497d4d1dd2e6236875bbd52/tests)配下を覗いてみると面白いです。

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

ただし、[オリジナルのBatsは2013年以降積極的にメンテナンスが行われていない](https://github.com/bats-core/bats-core#why-was-this-fork-created)ため、[Bats-core](https://github.com/bats-core/bats-core)を用いたほうがよさそうです。


# まとめ

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

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

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