Ansibleモジュールを自作したらunitテストをしてみよう

前回は [Ansibleモジュールを自作したらsanityテストをしてみよう](https://qiita.com/sky_jokerxx/items/b997117ea70bb8c8c1ec) という記事を書きましたが今回は自作したモジュールの `unitテスト` をしてみようと思います。
Ansibleのテストツールに組み込まれているunitテストは[pytest](https://docs.pytest.org/en/latest/)が使われています。

# 1. 環境

|   項目  | バージョン |
|---------|------------|
| OS      | CentOS7.5  |
| Python  | 3.6.6      |
| Ansible | devel      |

# 2. テスト用ドキュメント

* [https://docs.ansible.com/ansible/latest/dev_guide/testing_units.html#testing-units](https://docs.ansible.com/ansible/latest/dev_guide/testing_units.html#testing-units)

# 3. unitテストで使うソース

## 3-1. 作成したモジュール

前回作った `output_name.py` に手を入れてみます。
`sum` という関数を作って引数を足して戻すだけの関数を付け加えたものに対してユニットテストをしてみます。
イメージを持ってもらうということで簡単なものにしています。

```python:output_name.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright: (c) 2018, sky_joker
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function
__metaclass__ = type


ANSIBLE_METADATA = {
    'metadata_version': '1.1',
    'status': ['preview'],
    'supported_by': 'community'
}


DOCUMENTATION = '''
module: output_name
short_description: module that outputs a name.
description:
    - Output the specified name.
requirements:
    - python >= 2.7
options:
    name:
        description:
            - Specify name.
        required: True
'''

EXAMPLE = '''
---
- output_name:
    name: taro
  register: r

- debug: msg="{{ r }}"
'''

from ansible.module_utils.basic import AnsibleModule


def sum(a, b):
    return a + b


def main():
    argument_spec = dict(
        name=dict(type="str", required=True)
    )

    module = AnsibleModule(argument_spec, supports_check_mode=True)

    result = dict(changed=False)

    name = module.params['name']
    if(name):
        result['name'] = name
        module.exit_json(**result)
    else:
        module.fail_json(msg="Please specify name.")


if __name__ == "__main__":
    main()
```

## 3-2. テストモジュール

テストモジュールは以下のものを使用します。

```python:test_output_name.py
from ansible.modules.salf_made.output_name import sum

def test_sum():
    assert sum(1, 2) == 3
```

# 4. テストの準備

## 4-1. リポジトリのクローン

```shell-session
[example@localhost ~]$ git clone https://github.com/ansible/ansible.git
```

## 4-2. venv作成

テスト用にモジュールをインストールするのでvenvを作成します。

```shell-session
[example@localhost ~]$ cd ansible/
[example@localhost ansible]$ python36 -m venv venv
```

## 4-3. 必要なモジュールのインストール

```shell-session
[example@localhost ansible]$ source venv/bin/activate
(venv) [example@localhost ansible]$ yum -y install gcc python36-devel
(venv) [example@localhost ansible]$ pip3 install -r test/runner/requirements/units.txt
```

# 5. テスト実施

unitテストはテストモジュールから本体モジュールを呼び出す必要があるので、指定した場所に保存する必要があります。
ここでは以下の場所に保存します。

|         項目        |                  場所                  |
|---------------------|----------------------------------------|
| output_name.py      | $ANSIBLE/lib/ansible/modules/salf_made |
| test_output_name.py | $ANSIBLE/test/units/modules/salf_made  |

## 5-1. ディレクトリの作成及びモジュールの移動

ディレクトリの作成をします。

```shell-session
(venv) [example@localhost ansible]$ mkdir lib/ansible/modules/salf_made
(venv) [example@localhost ansible]$ mkdir test/units/modules/salf_made
```

本体のモジュール及びテストモジュールを作成します。

```shell-session
(venv) [example@localhost ansible]$ vi lib/ansible/modules/salf_made/output_name.py
(venv) [example@localhost ansible]$ vi test/units/modules/salf_made/test_output_name.py
```


## 5-2. テスト実施

`ansible` ディレクトリ内でテストを実行します。
`unitテスト` では、モジュール本体ではなくモジュール名をテストツールに引数で渡します。

成功すると以下のようになります。

![success_unit_test.gif](https://qiita-image-store.s3.amazonaws.com/0/180118/0be1be9d-d9d0-91be-567c-375542fe3748.gif)


```shell-session
(venv) [example@localhost ansible]$ ./bin/ansible-test units --python 3.6 output_name
Unit test with Python 3.6
============================================= test session starts ==============================================
platform linux -- Python 3.6.6, pytest-3.9.2, py-1.7.0, pluggy-0.8.0
rootdir: /home/example/ansible, inifile: tox.ini
plugins: xdist-1.23.2, forked-0.2
gw0 [1] / gw1 [1]
scheduling tests via LoadScheduling
.
--------------- generated xml file: /home/example/ansible/test/results/junit/python3.6-units.xml ---------------
=========================================== 1 passed in 0.33 seconds ===========================================
```

失敗(テストが想定外の値を返したりした場合)すると以下のようになります。

![fail_unit_test.gif](https://qiita-image-store.s3.amazonaws.com/0/180118/45bfed75-8488-cdd5-11a5-edbf842b8e58.gif)

```shell-session
(venv) [example@localhost ansible]$ ./bin/ansible-test units --python 3.6 output_name
Unit test with Python 3.6
============================================= test session starts ==============================================
platform linux -- Python 3.6.6, pytest-3.9.2, py-1.7.0, pluggy-0.8.0
rootdir: /home/example/ansible, inifile: tox.ini
plugins: xdist-1.23.2, forked-0.2
gw0 [1] / gw1 [1]
scheduling tests via LoadScheduling
F
=================================================== FAILURES ===================================================
___________________________________________________ test_sum ___________________________________________________
[gw0] linux -- Python 3.6.6 /home/example/ansible/venv/bin/python
def test_sum():
>       assert sum(1, 2) == 4
E       assert 3 == 4
E        +  where 3 = sum(1, 2)

test/units/modules/salf_made/test_output_name.py:6: AssertionError
--------------- generated xml file: /home/example/ansible/test/results/junit/python3.6-units.xml ---------------
=========================================== short test summary info ============================================
FAIL test/units/modules/salf_made/test_output_name.py::test_sum
=========================================== 1 failed in 0.34 seconds ===========================================
ERROR: Command "pytest --boxed -r a -n auto --color yes --junit-xml test/results/junit/python3.6-units.xml test/units/modules/salf_made/test_output_name.py" returned exit status 1.
```

# 6. 参考

* [https://docs.pytest.org/en/latest/](https://docs.pytest.org/en/latest/)