pythonでpytestを使ってテスト駆動開発するときのディレクトリ構造

# 概略

ただの箇条書きです。

一番重要なのは、`from ... import ...`が出来るかどうかです。
自作モジュールのディレクトリと、
テストモジュールのディレクトリ階層がどちらも深いときに。

３階層以上で挙動が変わるみたいです。

# ストーリー

以下の構造のように階層が深い時のサンプルコードが、
ググっても見つからなかったので記事にしました。

```bash
.
├tests
│├mod
││└test_module.py
├src
│├mod
││└module.py
```

以下のようなものは多く見かけました。

```bash
.
├tests
│└test_module.py
├src
│└module.py
```

ただ、「階層が深いから動かない」という理由もよくわからないです。
とりあえずこうすれば動く、ということは分かりました。
公式ドキュメントの文章が読みづらくて苦労してたどり着きました。

## なんか上手くいかない

やりたいことは、
[pytest でモジュールコードとテストコードをディレクトリで分離して unittest を実行するには？](https://qiita.com/keyangu/items/0fdaef693271647ba5b9)
なのですが、ここに書かれた方法だけでは、モジュール実行オプションが無いとimportできませんでした。

:::note warn
本記事はPython標準ライブラリの[unittest](https://docs.python.org/ja/3/library/unittest.html)とは無関係です。
:::

`launch.json`に、モジュールの数だけの全パターンのデバッグ構成を記述する必要があります。
チュートリアル程度の小さなプロジェクトなら問題なさそうですが、
プロジェクトが大きくなるほどナンセンスな方法と言えます。

# たどり着いたディレクトリ構造

`__init__.py`は不要です。`__init__.py`はpypiにライブラリを公開するときに考えるくらいで良さそう。
`__init__.py`が無くても正常に動作するし、インテリセンスも効きます。
今回は業務で自動化ツールを早いとこ作るためにテスト駆動するだけなので無視しても平気でしょう。（きっと）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134703/e8b331cd-3bdf-68aa-d133-51dc29c92d6e.png)

:::note warn
※大事なのは`.env`だけです。
:::

:::note info
Python 3.3以降では、[PEP420](https://peps.python.org/pep-0420/)で定義された[名前空間パッケージ（namespace packages）](https://packaging.python.org/ja/latest/guides/packaging-namespace-packages/)の導入により、このファイルがなくてもディレクトリがパッケージとして認識されるようになりました。

`__init__.py`ファイルは以下のような目的で使用されますが、必須ではありません：

- パッケージの初期化コードを実行する。
- パッケージレベルでの変数を定義する。
- パッケージ内のモジュールを特定の方法でインポートするためのショートカットを提供する。

しかし、`__init__.py`がない場合でも、Pythonはそのディレクトリをパッケージとして認識し、その中のモジュールやサブパッケージをインポートすることができます。これにより、より柔軟なパッケージ構造が可能となります。もちろん、引き続き`__init__.py`を使用してパッケージの初期化を制御したり、必要な情報を提供することもできます。
:::

# テスト

実際に行ったテストです。

```py:test_name.py
import pytest
from src.animal.mammal import human
from src.star import satellite

def test_human_name():
    target=human("Jane Doe")
    ans=target.name
    assert ans=="Jane Doe"

def test_satellite_name():
    target=satellite("lua")
    ans=target.name
    assert ans=="lua"

def test_human_foot_count():
    ans=human.howmanyfoot()
    assert ans==2

if __name__ == "__main__":
    pass
```

# テスト対象モジュール

```py:star.py
class satellite():

    def __init__(self, name:str):
        self.name=name

    def name(self) -> str:
        return self.name

class planet():

    def __init__(self, name:str):
        self.name=name

    def name(self) -> str:
        return self.name
```

```py:mammal.py
class human():
    def __init__(self, name:str):
        self.name=name

    @classmethod
    def name(self) -> str:
        return self.name

    @staticmethod
    def howmanyfoot() -> int:
        return 2
```

# パスの設定

「Currrent file デバッグ時」の`from import`の処理をするときのエラー回避策です。
これを置いてあげることで、「Pythonのパス」や「他の階層の自作モジュール」を認識してくれるみたいです。

```env:.env
PYTHONPATH=./
```

condaなどの仮想環境を使用していても認識してくれます。

以下のようなスクリプトを実行すると、
環境が認識できているか把握できます。

```py:import_test.py
import sys
print(sys.path)
```

上記の`import_test.py`の実行結果として、
`./src/`とかが入っていればOKです。

# デバッグ構成

３種類のデバッグ方法を定義しています。
上から順に、

- test_name モジュールをモジュールデバッグ
- mammalモジュールをモジュールデバッグ
- vscodeでカーソルを置いているファイルをデバッグ

私個人の見解ですが、モジュールデバッグは、
メインモジュールを指定する場合などの用途に限定されます。

多くの場合、編集中のタブで`F5`キーを押して、カレントファイルをデバッグする→テスト実行の流れです。

```json-doc:launch.json
{
    // IntelliSense を使用して利用可能な属性を学べます。
    // 既存の属性の説明をホバーして表示します。
    // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [

        {
            "name": "Python: モジュール test_name",
            "type": "python",
            "request": "launch",
            "cwd": "${workspaceFolder}",
            "module": "tests.test_mod.test_name"
        },{
            "name": "Python: モジュール mammal",
            "type": "python",
            "request": "launch",
            "cwd": "${workspaceFolder}",
            "module": "src.animal.mammal"
        },
        {
            "name": "Python: Current File",
            "type": "python",
            "request": "launch",
            "program": "${file}",
            "console": "integratedTerminal"
        }
    ]
}
```

# ワークスペース設定

- テストモジュールを置いているフォルダ
- どのテスト用フレームワークを使用するか
- python環境はどれを使用するのか記述したファイルを指定
  - condaなどの仮想環境を使用していてもこれで認識してくれます。

```json-doc:settings.json
{
    "python.testing.pytestArgs": [
        "tests"
    ],
    "python.testing.unittestEnabled": false,
    "python.testing.nosetestsEnabled": false,
    "python.testing.pytestEnabled": true,
    "python.envFile": "${workspaceFolder}/.env"
}
```

よくよく調べると、デフォルト設定で`.env`の場所は指定されているんですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134703/60d8208b-3ea7-eee6-67e1-b190dc420d44.png)

# テスト結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134703/d06ddc2d-9057-78cd-a137-b71e12db5fce.png)

# 環境

- Windows 10 Latest
- Python 3 Latest
- Miniconda Latest
- Pip Latest
- pytest Latest

※`conda`を利用しているのは、「仮想環境の切り分け」・「仮想環境へのPythonインストール」までです。
仮想環境内でライブラリのインストールは`pip`を利用しています。

# まとめ

pyhonムズイ。
C系のnamespaceが恋しいです。

# 続き

こちらの記事のほうが後に書いたのでもしかしたら参考になるかもです。
ぜひご覧ください。

https://qiita.com/ShortArrow/items/473da9a3b61f283c532a

**Excelsior!**