AnsibleロールのユニットテストからTravis CIまで

> 続編の記事を書きました、本記事内のセットアップ手順が自動化されています
> [git-flowを利用したAnsibleロールのテスト駆動開発の流れ](http://qiita.com/tumf/items/194b0b764cfd474e3398)


ユニットテストのしやすい`Ansible`ロールのの書き方から`Travis CI`に載せるまで解説します。本稿に書いてある方法は最近思いついたのですが、複数のテストケースに対応し、ロール単体で開発ができるため大変重宝しています。

> **Note**
> `Ansible`自体のインストールは別の記事に譲ります。
>
>     $ pip install ansible
>

## `sample`ロールの作成

> **Note**
> 本稿のサンプルは以下のURLより入手出来ます
> https://github.com/tumf/ansible-unit-test-sample

例として`ansible-galaxy`コマンドで`sample`ロールの雛形を作ります。

    $ ansible-galaxy init sample                      

以下の様なファイルが生成されます。

```
├── README.md
├── defaults
│   └── main.yml
├── files
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── tasks
│   └── main.yml
├── templates
└── vars
    └── main.yml
```

## テストの準備

ここに`tests`ディレクトリを作成してここにユニットテストをするためのplaybook等を準備します。playbookは`test.yml`という名前にして以下のように書きます。

```yaml:tests/test.yml
---
- hosts: 127.0.0.1
  connection: local
  tags:
    - case-1
  vars:
    ansible_unit_test: True
  roles:
    - role: ../..
```

まず、最初の2行...

```
- hosts: 127.0.0.1
  connection: local
```
この部分はテストをローカルで動かす為のお約束です。以下ポイントとなる部分は2つです。

**ポイント1** プレイブックに`case-1`というタグをつけます。

```
  tags:
    - case-1
```

これがテストケース名になります。(case-1は例です分かりやすい名前をつけて下さい)


**ポイント2** `ansible_unit_test`の変数を設定します。

```
  vars:
    ansible_unit_test: True
```
これはテスト時にスキップしたいタスクに`when: is not ansible_unit_test`をつけて実行しないために用意しました。

次に、作成するロールがほかのロールに依存している場合、`tests/requirements.yml`に以下のように書いて...

```yaml:tests/requirements.yml
- src: tumf.systemd-service
```

`ansible-galaxy`コマンドでインストールします。

    $ ansible-galaxy install -r tests/requirements.yml -p tests/roles

## テストしやすいロールの書き方

ロールをテストしやすくするために、出力パスの装飾とスキップタスクの指定を行います。


### 出力パスの装飾

ロールのタスクを`tests/cases/テストケース名`以下にインストールするように作成します。`prefix_dir`という変数を作って、ロールの中のファイルパスを**全て修飾**します。

まず、ロールのdefaultに以下の様に書いておき...

```defaults/main.yml
prefix_dir: ""
```

templateのdest等で出力先のパスの頭に`prefix_dir`を置きます。

```tasks/main.yml
- template: src="default.j2" dest="{{ prefix_dir }}/etc/default/sample.j2"
  notify: reload systemd
```

### スキップタスクの指定

テスト時に本番でないと実施出来ないコマンドなどスキップしたいタスクに`when: is not ansible_unit_test`をつけて実行しないために用意しました。`ansible-playbook`時に`-C`つければいいかと思ったのですが、実行環境によってモジュールが読み込まれないことがあります（私は、本番Linuxに対しOSX上でテストを回しています)。

以下のように` when: not ansible_unit_test`をつけていきます。

```
- service: name="sample" state=started enabled=yes
  when: not ansible_unit_test
```

実際本番に対して利用する時のためにデフォルトで`ansible_unit_test`を`False`に設定します。

```yaml:defaults/main.yml
ansible_unit_test: False
```


## テストの実行スクリプト

次に、テスト実行用のスクリプト`tests/run`を用意します。

```bash:tests/run
#!/bin/bash

usage_exit() {
        echo "Usage: $0 [-w] name" 1>&2
        exit 1
}

check="-C"
while getopts wh option
do
    case $option in
        w)
            check=""
            ;;
        h)
            usage_exit
            ;;
    esac
done

shift $((OPTIND - 1))

mkdir -p tests/cases
cases=$(ls tests/cases)
if [ ! -z $1 ];then
    cases=$1
fi
errors=0

for case in $cases
do
    out=$(ansible-playbook ./tests/test.yml -i 127.0.0.1, -t $case -D $check -e prefix_dir="cases/${case}")
    result=$?
    if echo $out|tail -n 1 |grep -E "changed=0\s+unreachable=0\s+failed=0" >/dev/null
    then
        echo -n "."
    else
        echo $case
        echo
        echo "$out"
        errors=$(( errors+1 ))
    fi
done

if [ $errors -eq 0 ]
then
    echo " ok"
else
    echo "${errors} error(s)"
fi
exit $errors
```
## テストの実行

テストは以下のようにして実行します。

```
$ ./tests/run case-1            
case-1


PLAY [127.0.0.1] 
GATHERING FACTS *************************************************************** 

ok: [127.0.0.1]

TASK: [../.. | template src="default.j2" dest="{{ prefix_dir }}/etc/default/sample"] *** 
--- before: cases/case-1/etc/default/sample
+++ after: /Users/tumf/tmp/sample/templates/default.j2
@@ -0,0 +1 @@
+default test

changed: [127.0.0.1]

TASK: [../.. | service name="sample" state=started enabled=yes] *************** 
skipping: [127.0.0.1]

NOTIFIED: [../.. | reload systemd] ******************************************** 
skipping: [127.0.0.1]

PLAY RECAP ******************************************************************** 
127.0.0.1                  : ok=2    changed=1    unreachable=0    failed=0   
1 error(s)
```

`case-1`は`tests/test.yml`で`tags`に指定したテストケースです。この段階で最後のエラーは気にしないでください。上の出力を見て正しく生成されているか確認してください。正しく生成されるまでプレイブックの修正を行います。

##テストケースを作成する。

テストケース`case-1`が生成するファイル`tests/cases/case-1/etc/default/sample`を正しいケースとしてテストケースに登録します。以下のように実行します。

```
$ ./tests/run -w case-1
(略)
```

正しく実行できれば（できるはずですが）`tests/cases/case-1/etc/default/sample`が実際に書き込まれます。次回より以下のようにしてテストを実行します。

```
$ ./tests/run  case-1   
. ok
```

> **Note** ケース名を省略すると、`tests/cases`以下をスキャンして全て実行します。通常はこちらのほうが便利です。
> 
>     $ ./tests/run
>     . ok
> 

## テストケースを増やす

あとはテストケースを必要に応じて増やしていきます。
以下は`var`を変えただけの単純な例です。

```yaml:tess/test.yml
---
- hosts: 127.0.0.1
  connection: local
  tags:
    - case-1
  vars:
    ansible_unit_test: True
    var: var of case-1
  roles:
    - role: ../..
- hosts: 127.0.0.1
  connection: local
  tags:
    - case-2
  vars:
    ansible_unit_test: True
    var: var of case-2
  roles:
    - role: ../..
```

以下のコマンドを実行し...

```
$ ./tests/run case-2
```

正常に動いたら、テストケースとして登録します。

```
$ ./tests/run -w case-2
```

以後は、以下のコマンドで`case-1``case-2`同時にチェックします。

```
$ ./tests/run           
.. ok
```

簡単ですね。

### テストでエラーを発生させる

`case-1`のテストケースが登録されている状態で、`var`を変えるとテストケースとの間に差分が出るはずです。この差分が検出されるとエラーとなります。

プレイブックを以下のように修正します。

```yaml:tests/test.yml
  vars:
    ansible_unit_test: True
    var: var of case-one # ここを変えた
```

テストを実行します。すると以下のようにエラーが報告されます。

```bash:
$ ./tests/run 
case-1


PLAY [127.0.0.1] ************************************************************** 

GATHERING FACTS *************************************************************** 

ok: [127.0.0.1]

TASK: [../.. | file state="directory" path="{{ prefix_dir }}/etc/default"] **** 

ok: [127.0.0.1]

TASK: [../.. | template src="default.j2" dest="{{ prefix_dir }}/etc/default/sample"] *** 
--- before: cases/case-1/etc/default/sample
+++ after: /Users/tumf/tmp/sample/templates/default.j2
@@ -1 +1 @@
-default test var of case-1
+default test var of case-one

changed: [127.0.0.1]

TASK: [../.. | service name="sample" state=started enabled=yes] *************** 
skipping: [127.0.0.1]

NOTIFIED: [../.. | reload systemd] ******************************************** 
skipping: [127.0.0.1]

PLAY RECAP ******************************************************************** 
127.0.0.1                  : ok=3    changed=1    unreachable=0    failed=0   
.1 error(s)
````

この差分が意図通りであれば`./tests/run -w case-1`で新たなテストケースを書き込んで下さい。間違いであれば、ロールを修正して下さい。

このような感じでテストを繰り返しつつプレイブックを意図通りに完成させていきます。


## Travis CI

ここまでやると、CIを利用した自動テストを実行したくなると思います。
`Travis CI`ならば`.travis-ci.yml`を以下のように書くだけです。

```yaml:.travis-ci.yml
language: python
python:
- '2.7'
install:
- pip install ansible
#- ansible-galaxy install -r tests/requirements.yml -p tests/roles
before_script:
- ansible --version
- ansible-playbook --syntax-check ./tests/test.yml -i ./tests/hosts
script:
- ./tests/run
```

以下の部分がコメントアウトされています。

```
#- ansible-galaxy install -r tests/requirements.yml -p tests/roles
```

このロールが他のロールに依存していない時すなわち`tests/requirements.yml`が空になると、`ansible-galaxy`がエラーになるため、コメントアウトしています。依存するロールがあり、`tests/requirements.yml`を書いた場合はコメントアウトを外して下さい。

以上

>以下の記事は大変参考になりました、ありがとうございます！
> TravisCIでAnsibleのrole単体のテストを実行する
> http://qiita.com/djyugg/items/627aa88e02422612f164