TravisCI
Ansible

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

More than 1 year has passed since last update.

続編の記事を書きました、本記事内のセットアップ手順が自動化されています
git-flowを利用したAnsibleロールのテスト駆動開発の流れ

ユニットテストのしやすい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という名前にして以下のように書きます。

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に以下のように書いて...

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_testFalseに設定します。

defaults/main.yml
ansible_unit_test: False

テストの実行スクリプト

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

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

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

テストケース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を変えただけの単純な例です。

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-1case-2同時にチェックします。

$ ./tests/run           
.. ok

簡単ですね。

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

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

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

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

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

$ ./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を以下のように書くだけです。

.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