プログラミング初心者がアーキテクトっぽく語る

見苦しい記事も多数あるとは思いますが訂正しつつブログと共に成長していければと思います

PyCharmでテストコードのフォルダを多階層にしたらテストが動かなくなった

PyCharm + unittestでTDDっぽいことを試しました。最後にテストコードのフォルダ階層をプロジェクトコードのフォルダ階層と一致させたらそれまで動いていた単体試験の大半が動かなくなってしまいました。

ただのsys.pathの理解不足だったのですが同じことで困っている人がいるかもしれなので投稿します。


コード完成

PyCharmでTDDっぽくコードを作成し、完成させました。

そのときの構成は以下のような感じです。__init__.pyは省略してます。

myproject/
├── packageA
│   ├── subpackageA
│   │   └── moduleA.py
│   └── subpackageB
│       └── moduleB.py
└── tests
    ├── test_moduleA.py
    └── test_moduleB.py
    └── test_data.json

プロジェクトコードはmyproject/packageA/に置き、テストコードとテストデータはmyproject/tests/に置きました。


テストコードのフォルダ階層を変更

testsフォルダには1階層しかありません。これで困っていたわけではありませんがJavaっぽくテストコードをプロジェクトコードと同じ多階層構造にしようと思いました。

Javaではテストコードをプロジェクトコードと同じ構成にすることが多いと思います。というかJavaIDEがそれを推奨してくることが多いと思います。

変更の結果がこちらです。

myproject/
├── packageA
│   ├── subpackageA
│   │   └── moduleA.py
│   └── subpackageB
│       └── moduleB.py
└── tests
    ├── packageA
    │   ├── subpackageA
    │   │   └── test_moduleA.py
    │   └── subpackageB
    │       └── test_moduleB.py
    └── test_data.json

myproject/tests/配下の構造がプロジェクトコードと同じになりました。これでどのプロジェクトコードのテストがどこにあるかひと目でわかるようになりました。


単体試験が動かない!

しかしさっきまで動いていた単体試験の大半が動かなくなってしまいました。理由はimportエラーです。

かなり悩みましたが問題はsys.pathでした。PyCharmも少し関わってます。


相対パスはない

プロジェクトコードは動くのに単体試験でプロジェクトコードのimportエラーが出るとき真っ先に疑うべきは相対パスです。

プロジェクトコードでもテストコードでも相対パスは使うべきではありません。理由は他の方々が詳しく書いてくれています。

https://qiita.com/jesus_isao/items/f93c11248192645eb25d

https://comeroutewithme.com/2018/02/24/python-relative-imports-and-unittests-derp/

なお私はこれには該当しませんでした。


プロジェクトコードと同じパッケージ名で勘違い

テストコードtest_moduleA.pyがプロジェクトコードpackageA.subpackageA.moduleAをimportできません。その理由を調査します。

以下の文をテストコードtest_moduleA.pyに仕込んでsys.pathを表示しました。

import sys
print(sys.path)

見やすいように改行と行番号を入れます。

1: [
2:  '/Users/john/PycharmProjects/myproject/tests',
3:  '/Users/john/PycharmProjects/myproject',
4:  '/Applications/PyCharm CE.app/Contents/plugins/python-ce/helpers/pycharm',
5:  '/Users/john/.pyenv/versions/3.8.0/lib/python38.zip',
6:  '/Users/john/.pyenv/versions/3.8.0/lib/python3.8',
7:  '/Users/john/.pyenv/versions/3.8.0/lib/python3.8/lib-dynload',
8:  '/Users/john/PycharmProjects/test_unittest/.venv/lib/python3.8/site-packages'
9: ]

4行目以下のPathはmyprojectとは全く関係がありません。packageA.subpackageA.moduleAをimportするために頼れるのは2、3行目だけです。この2行に注目しましょう。

2行目のPathを見るとmyproject/testsです。myproject/testsの構成は以下の通りです。

myproject/tests/
├── packageA
│   ├── subpackageA
│   │   └── test_moduleA.py
│   └── subpackageB
│       └── test_moduleB.py
└── test.json

Loaderの気持ちになってこのフォルダからpackageA.subpackageA.moduleAを探してみましょう。

あ、、、packageA.subpacakgeAってある。

これは絶対パスではtests.packageA.subpacakgeAだけどmyproject/testsの中から見るとpackageA.subpacakgeAかもしれない。

myproject/tests/packageAをmyproject/tests/packageA_に変更して試したら、、、動いた!

tests.packageA.subpacakgeAをプロダクションコードのpackageA.subpacakgeAと勘違いしたことがimportエラーの原因でした。


対策調査

原因はわかりましたが解決にはなってません。フォルダ名はプロジェクトコードと同じにしたいのです。調査を続けます。

フォルダ名を変更したくないならsys.pathを変えるしかありません。

2行目のパスはPyCharmのEdit ConfigurationのWorking directoryの値です。そこでWorking Directoryをmyproject/に変更しました。以下がそのときのsys.pathです。

1: [
2:  '/Users/john/PycharmProjects/myproject',
3:  '/Users/john/PycharmProjects/myproject',
4:  '/Applications/PyCharm CE.app/Contents/plugins/python-ce/helpers/pycharm',
5:  '/Users/john/.pyenv/versions/3.8.0/lib/python38.zip',
6:  '/Users/john/.pyenv/versions/3.8.0/lib/python3.8',
7:  '/Users/john/.pyenv/versions/3.8.0/lib/python3.8/lib-dynload',
8:  '/Users/john/PycharmProjects/test_unittest/.venv/lib/python3.8/site-packages'
9: ]

2行目がmyprojectになりました。以下がmyprojectの構成です。

myproject/
├── packageA
│   ├── subpackageA
│   │   └── moduleA.py
│   └── subpackageB
│       └── moduleB.py
└── tests
    ├── packageA
    │   ├── subpackageA
    │   │   └── test_moduleA.py
    │   └── subpackageB
    │       └── test_moduleB.py
    └── test_data.json

Loaderの気持ちになってこのフォルダからpackageA.subpackageA.moduleAを探してみましょう。

前回勘違いしたテストコードは今回は「tests.packageA.subpackageA」と見えます。勘違いすることはありません。間違えることなくプロダクションコードのpackageA.subpackageA.moduleAにたどり着きます。これなら問題なさそうです。

テストを実行したら予想通り動きました。


テストデータが見つからない

Working directoryをmyproject/に変更したら、テストデータの読み込みに失敗するテストコードがいくつか生まれました。

一難去ってまた一難。心が萎えます。

幸いすぐに原因はわかりました。

変更前のWorking directoryはmyproject/testsだったのでテストコードはopen('./test_data.json', 'r')でテストデータを読み込めました。

Working directoryがmyprojectになったのでopen('./tests/test_data.json', 'r')と変更する必要がありました。そこまではPyCharmが面倒をみてくれなかったわけです。


結論

プロダクションコードとテストコードのフォルダ構成を同じにしたときは、sys.pathの先頭にproject_root/tests/が来ないようにしましょう。PyCharmの場合はEdit ConfigurationのWorking directoryで変更できます。

個人的には全てのテストコードはproject_rootをWorking Directoryとして実行できるようにimport文やファイル読み込み処理を実装するとわかりやすいと思いました。

そのためのチェックとしてproject_root/testsのWorking directoryをproject_root/にしてproject_root/testsを右クリックして全ての試験を実行するとよいと思います。

単体試験をするときはsys.pathとPyCharmのWorking directoryにご注意を!