Python+pytestでテスト駆動開発しようとしたらテストの実行で躓いたので解決法をメモ

業務でPythonによるテスト駆動開発(TDD)を行うことになり、pytestを用いました。ただ、ディレクトリ構造が正しく認識されないと<strong><font color="red">ModuleNotFoundError</font></strong>に阻まれて実装が進まなくなってしまうため、備忘録を兼ねてまとめることにしました。

# 環境構築
pytestのインストールが必要です。下記コマンドの実行でインストールが可能です。

```bash
pip install pytest
```

pytestを毎回手動実行するのは煩雑であるため自動実行できる様にしておこうと思い、pytest-watchもインストールしました。起動しておくとソースが更新される毎に自動でpytestによるテストが実行されます。

```bash
pip intall pytest-watch
```
起動方法

```bash
ptw
```

# 最も簡単な方法

最も簡単な方法はソースファイルとテストソースを同じディレクトリ内に格納する方法です。これ以降、以下のサンプルソースを使って説明します。空ファイルに近い内容ですが、pytestが実行されているかの確認には使えます。

## ソースファイル(hogehoge.py)

```python
class HogeHoge:
    pass
```


## テストソース(test_hogehoge.py)

```python
from hogehoge import HogeHoge

def test_init():
    HogeHoge()
```
pytestはtest_hogehoge.pyまたはhogehoge_test.pyというファイル名を探して実行される仕様となっているので必ずテストソースのファイル名には<strong><font color="RED">test</font></strong>の文言を含めましょう。別のターミナルをもう1つ起動しソースが格納されているディレクトリに移動した上でpytest-watchを起動してみましょう。pytestが実行されます。

<details><summary>正常にテストが完了するとこの様な表示が出ます</summary><div>

```
============================= test session starts ==============================
platform darwin -- Python 3.7.3, pytest-5.1.1, py-1.8.0, pluggy-0.12.0
rootdir: sample-tdd
collected 1 item 
test_hogehoge.py .                                                       [100%]

============================== 1 passed in 0.01s ===============================
```
</div></details>

# ソースディレクトリを切り分ける方法

ソースファイルが少ない時は上記の方法でも良いでしょう。しかし、ソースファイルが増えればソースファイルとテストソースが混在してしまい、目的のソースファイルが探しにくくなります。故にソース用ディレクトリとテスト用ディレクトリに切り分けます。

### ディレクトリの切り分け

今回は以下の様なディレクトリ構造にしました。pytestはカレントディレクトリかtestsというディレクトリにテストを探しに行く仕様となっている様なので、testsというディレクトリ名にしています。

```
sample_tdd/
├── modules
│   └── hogehoge.py
└── tests
    └── test_hogehoge.py
```

### `__init__.py`の準備

modulesディレクトリに__init__.pyを格納します。記載内容は以下の通りです。

```python
from .hogehoge import HogeHoge

__all__ = [

    'HogeHoge',

]
```

### テストソースの修正

test_hogehoge.pyの内容を以下の様に修正します。

```diff
- from hogehoge import HogeHoge
+ from modules.hogehoge import HogeHoge

def test_init():
    HogeHoge()

```

結果として以下の様なディレクトリ構造になりました。

```
sample_tdd/
├── modules
│   ├── __init__.py
│   └── hogehoge.py
└── tests
    └── test_hogehoge.py
```

pytest-watchを実行すると以下の様な結果となりました。

```
______________ ERROR collecting sample_tdd/tests/test_hogehoge.py ______________
ImportError while importing test module 'sample_tdd/tests/test_hogehoge.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
sample_tdd/tests/test_hogehoge.py:1: in <module>
    from modules.hogehoge import HogeHoge
E   ModuleNotFoundError: No module named 'modules'
!!!!!!!!!!!!!!!!!!! Interrupted: 1 errors during collection !!!!!!!!!!!!!!!!!!!!
=============================== 1 error in 0.05s ==============================
```

### conftest.pyを追加

プロジェクトのrootディレクトリ(この例ではsample_tddディレクトリ)に空ファイルconftest.pyを追加します。追加には以下のコマンドを実行します。これはpytestの仕様上必要なファイルです。

```bash
touch conftest.py
```

ディレクトリ構造は以下の様になりました。

```
sample_tdd/
├── conftest.py
├── modules
│   ├── __init__.py
│   └── hogehoge.py
└── tests
    └── test_hogehoge.py
```

pytest-watchの実行結果が以下の様に変わり正常にテストが終了しました。

```
============================= test session starts ==============================
platform darwin -- Python 3.7.3, pytest-5.1.1, py-1.8.0, pluggy-0.12.0
rootdir: sample_tdd
collected 1 item                                                               

sample_tdd/tests/test_hogehoge.py .                                      [100%]

============================== 1 passed in 0.02s ===============================
```


# まとめ
pytest、pytest-watchを使ってのPythonによるテスト駆動開発を実施するための前準備として環境構築を行い、簡単なサンプルプログラムを実行し、正しくテストが実施されることを確認しました。今回記載したpytestの仕様上の規則等についても今後随時追加していきたいと思います。

# 階層型ディレクトリ構造の場合 (2019/9/26追記)

階層型ディレクトリ構造の場合(以下のソース例)のような場合のモジュールのインポートの方法を追記します。

* 階層型ディレクトリ構造のソースの例

```
sample_source/
├ calculator
│   ├ __init__.py
│   ├ calculator.py
│   └ modules
│       ├ __init__.py
│       ├ add.py
│       ├ division.py
│       ├ multiple.py
│       └ subordinate.py
├ conftest.py
└ tests
    └ calculator
        ├ test_add.py
        ├ test_calculator.py
        ├ test_division.py
        ├ test_multiple.py
        └ test_subordinate.py

```

`test_calculator.py`から`calculator.py`を読み込む場合

```python
from calculator import Calculator
```

`test_add.py`から`add.py`を読み込む場合

```python
from calculator.modules import Add
```

テストが正しく実行されています。

```
========================= test session starts =========================
platform linux -- Python 3.7.4, pytest-5.1.1, py-1.8.0, pluggy-0.12.0  
rootdir: sample_source
collected 4 items                                                      

tests/calculator/test_add.py ..                                 [ 50%] 
tests/calculator/test_calculator.py ..                          [100%] 

========================== 4 passed in 0.70s ==========================
```

# Reference

* [通す - python モジュール 追加](https://code-examples.net/ja/q/9c7602)
* [[Python] importの躓きどころ](https://qiita.com/ysk24ok/items/2711295d83218c699276)
* [pytestの使い方と便利な機能](https://qiita.com/sasaki77/items/97c90ae272373d78b422)