これは私がテスト駆動開発を身につけるまでの長い旅の始まり
解決策
アップデート版 解決策 (2019/07/13)
-
__init__.py
を配置して、test
ディレクトリ以下をモジュール化 -
python -m unittest
を実行することで、全てのテストが実行されます
. # カレントディレクトリ
├── practice_test
│ └── tashizan.py
└── test
├── __init__.py # 追加
└── practice_test
├── __init__.py # 追加
└── test_tashizan.py
python -m unittest
はpython -m unittest discover -s . -p test*.py
と同じで
カレントディレクトリ以下のtest*.py
というファイル名のパターンに該当するテストを実行しているそうです。
詳しく知りたい方はドキュメントの「テストディスカバリ」をご参照ください。
以上は、https://twitter.com/kashew_nuts/status/1148671204405891072 にて教えていただきました。
kashew_nutsさんに感謝申し上げます。
アップデート版に対応するソースコードはこちらです。
https://github.com/ftnext/test_first_time/tree/correct-way
以下は、2019/07/09に公開した内容です。
以下の方法でもModuleNotFoundErrorは解決されたのですが、python -m unittest
が効かず(__init__.py
がないためと推測)、テストはファイルごとに実行するようになってしまったので、今後はアップデート版を使っていきたいと思います。
当初の解決策 (2019/07/09)
解決手順をざっくり書くと
- setup.py, setup.cfgの2つのファイルを作成
-
pip install -e .
を実行
の2ステップです。
この説明でピンと来ない方は「解決するまで」の部分をご確認ください。
(ピンと来る方はこの記事から得られるものは少ないのではないかと思います)
前提
動作環境
- macOS 10.14.4
- Python 3.7.3
注意
- 絶対パスを表示する場合、PCのユーザ名は...で置き換えています
- 筆者はPythonを1年半ほど使っていますが、今回パッケージのimport周りの理解が甘かったことを痛感しました。記載に誤り等ありましたらご指摘いただけますと助かります。
背景
unittestの使い方に習熟するべく、Qiita記事(1)「Python標準のunittestの使い方メモ」の写経に取り組んでいました。
元の記事(1)はPython2系で動かしていますが、手元ではPython3系で動かしました。
元の記事(1)が想定していると思われるファイル配置は以下です。
.
├── tashizan.py
└── test_tashizan.py
「テストコードとテスト対象のコードを同じ階層に置かずに、それぞれディレクトリに分けて配置したい」と思い、以下のようなディレクトリ分割を試してみることにしました。
. # カレントディレクトリ
├── practice_test # テスト対象のPythonのファイルを置く
│ └── tashizan.py
└── test # テストを置く
└── practice_test # practice_test内のPythonファイルのテストを置く
└── test_tashizan.py
# 記事(1)の通りです
def tashizan(a, b):
"""calculate (a + b)
"""
return a + b
import unittest
from practice_test.tashizan import tashizan
class TestTashizan(unittest.TestCase):
"""test class of tashizan.py
"""
def test_tashizan(self):
"""tashizanメソッドで足し算が行われるか確認する
"""
value1 = 3
value2 = 5
expected = 8
actual = tashizan(value1, value2)
self.assertEqual(expected, actual)
if __name__ == '__main__':
unittest.main()
カレントディレクトリからテストコードを実行したところ、以下のようにModuleNotFoundErrorが発生しました😱
$ python test/practice_test/test_tashizan.py
Traceback (most recent call last):
File "test/practice_test/test_tashizan.py", line 3, in <module>
from practice_test.tashizan import tashizan
ModuleNotFoundError: No module named 'practice_test'
このエラーの解決に取り組んで知ったことをこの記事に記します。
原因
インタラクティブシェルで実験したところ、test/practice_test
ディレクトリの中でscripts
というモジュール(※)を探すも、見つからずにエラーになっているらしいと理解しました。
※ここで、モジュールとは、Pythonのファイル1またはPythonのファイルが置かれたディレクトリ2という認識です。
$ python # 前提: 上記の.をカレントディレクトリとしている
>>> import practice_test # importできる
>>> exit()
$ cd test/practice_test/
$ python
>>> import practice_test # importできない
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'practice_test'
どうやらimportでは、実行されたPythonのファイルのあるディレクトリの中でモジュール(ディレクトリ)をimportしようとするようです。
(カレントディレクトリによらずに実行されたPythonのファイルの位置を起点にしているらしいとわかりました)
解決するまで
参考にしたのは 「Pythonのパッケージングのベストプラクティスについて考える2018」 という記事(2)です。
practice_test
ディレクトリ内のPythonのファイルをパッケージとしてインストールしたことで、test/practice_test/test_tashizan.py
の中でpractice_test
をimportできるようになったと理解しています。
1.パッケージとしてインストールするために
setup.pyとsetup.cfgの2つのファイルを作ります。
記事(2)中の「5-4 setup.cfg に setup 関数の引数を書く」を参考にしています。
from setuptools import setup
setup()
[metadata]
name = test_first_time
version = 0.0.1
description = テストコードをディレクトリに分けて配置する構成の練習
keywords = unittest
classifiers =
Programming Language :: Python :: 3
Programming Language :: Python :: 3.7
[options]
install_requires =
numpy
[options.extras_require]
dev =
flake8
pytest
作成後のファイルの配置は以下のようになります。
.
├── practice_test
│ └── tashizan.py
├── setup.cfg # 追加
├── setup.py # 追加
└── test
└── practice_test
└── test_tashizan.py
2.パッケージとしてインストール
$ pip install -e .[dev] # 前提: 上記の.をカレントディレクトリとしている
$ python
>>> import practice_test # 相変わらずimportできる
>>> exit()
$ cd test/practice_test/
$ python
>>> import practice_test # importできるようになった
>>> exit()
記事(2)によると、
- setup.pyのあるディレクトリで
pip install .
を実行すると、パッケージがインストールされる - 開発者向けのパッケージのインストールの方法が
pip install -e '.[dev]'
3(-e
はPythonファイルへの変更が即座にパッケージの変更として反映される指定。dev
でsetup.cfgのextras_require
にdev
として指定したパッケージがインストールされる)
とのことです。
エラーが解決された様子
テストが実行できるようになりました!
$ python test/practice_test/test_tashizan.py # 上記の.をカレントディレクトリとしている
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
$ pytest # extras_requireのdevの指定でpytestも入れたので実行可能
============================= test session starts ==============================
platform darwin -- Python 3.7.3, pytest-5.0.1, py-1.8.0, pluggy-0.12.0
rootdir: /Users/.../study/test_first_time
collected 1 item
test/practice_test/test_tashizan.py . [100%]
=========================== 1 passed in 0.04 seconds ===========================
格闘の末に得たコードの全容は以下のリポジトリで確認できます:
https://github.com/ftnext/test_first_time/tree/902a8b12f677b7eefa5929ee65a185f876436e1e
pip install -e .
前後の変化
sys.path
に追加がありました。
実行前(見やすくなるようにprintしたリストに改行を入れました)
>>> import sys
>>> print(sys.path)
['', '/Library/Frameworks/Python.framework/Versions/3.7/lib/python37.zip',
'/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7',
'/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload',
'/Users/.../study/test_first_time/env/lib/python3.7/site-packages']
実行後:setup.pyを置いたディレクトリtest_first_time
の絶対パスが追加されています
>>> import sys
>>> print(sys.path)
['', '/Library/Frameworks/Python.framework/Versions/3.7/lib/python37.zip',
'/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7',
'/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload',
'/Users/.../study/test_first_time/env/lib/python3.7/site-packages',
'/Users/.../study/test_first_time'] # 追加
Pythonのドキュメント3中に
パッケージを import する際、 Python は sys.path 上のディレクトリを検索して、トップレベルのパッケージの入ったサブディレクトリを探します。
という記載があります。
'/Users/.../study/test_first_time'
がsys.path
に追加されたので、practice_test
がimportできるようになったと考えています。
合わせて読んだ参考記事
(2)と合わせて、記事(3)「Pythonのパッケージ周りのベストプラクティスを理解する」も参考にしました。
(3)を読んだところ
- setup.py はパッケージをインストールする仕組み
- setup.pyが複雑になる問題の対応として、setup()のパラメータをsetup.cfg に書けるようにした
と理解しました。
pip install hoge
で自動でPyPIからhogeをダウンロード&ZIPファイルを展開しsetup.py
を実行してくれます。((3)内の記載)
pip install .dev
では、setup.pyを実行してインストールをしたと理解しています。
今後詰めていきたいところ
今回「Done is better than perfect」の精神でアウトプットしました。
理解が深まった際は、この記事を更新していきます。
以下に理解が甘い点や、確認したいドキュメントを記載します。
-
pip install -e .
はpython setup.py develop
4と同じ?(pipコマンドとsetup.pyの対応関係を知りたい) - setup.cfgの書き方:https://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files
-
「(前略)Python では定義をファイルに書いておき、スクリプトの中やインタプリタの対話インスタンス上で使う方法があります。このファイルを モジュール (module) と呼びます。」(https://docs.python.org/ja/3/tutorial/modules.html より) ↩
-
ディレクトリがパッケージであり、パッケージはモジュールの一形態と認識しています。「パッケージはファイルシステムのディレクトリ、モジュールはディレクトリにあるファイルと考えることができますが、(後略。これは比喩とも書かれている)」「(前略)他の言い方をすると、パッケージは単なる特別な種類のモジュールであると言えます。」(いずれも https://docs.python.org/ja/3/reference/import.html#packages より) ↩
-
https://setuptools.readthedocs.io/en/latest/setuptools.html#develop-deploy-the-project-source-in-development-mode ↩