pytestにて、プライベート関数のテストで悩んだことがあったため、メモを残します。
なお、今回のテスト対象コードは以下とします。
target.py
def __double_underscore_function(): return 'double'
目次
環境
- Windows10
- Python 3.6.0 (32bit)
- pytest 3.0.5
プライベート関数のimportについて
通常のコードの場合
Pythonでは変数・関数・メソッド名の先頭に_
(アンダースコア)があると、それらはプライベートなものとして扱われます。
- 2.3.2. 予約済みの識別子種 | 2. 字句解析 — Python 3.5.2 ドキュメント
- 7.11. import 文 | 7. 単純文 (simple statement) — Python 3.5.2 ドキュメント
ただ、プライベート関数の場合
standard_usage.py
from target import __double_underscore_function import target def main(): print(f'from import: {__double_underscore_function()}') print(f'import: {target.__double_underscore_function()}') if __name__ == '__main__': main()
と関数の中で使った場合でも
>python standard_usage.py from import: double import: double
と正常に動作します。
一方、
from target import __double_underscore_function class Main(object): def run_with_import_from(self): print(f'from import: {__double_underscore_function()}') def run_with_import(self): print(f'import: {target.__double_underscore_function()}') if __name__ == '__main__': m = Main() m.run_with_import()
と、クラスの中で使った場合
- run_with_import_from()の場合、
NameError: name '_Main__double_underscore_function' is not defined
- run_with_import()の場合、
AttributeError: module 'target' has no attribute '_Main__double_underscore_function'
という例外が送出されます。
マングリングっぽい動きです。
9.6. プライベート変数 | 9. クラス — Python 3.5.2 ドキュメント
テストコードの場合
テストコードの場合も同様で、
test_pytest_ver.py
import pytest from target import __double_underscore_function import target class Test_function(object): def test_double_underscore_prefix_function_using_from_import(self): assert __double_underscore_function() == 'double' def test_double_undersocre_prefix_function_using_import(self): assert target.__double_underscore_function() == 'double'
と書くと、
>pytest test_pytest_ver.py ============================= test session starts ============================= platform win32 -- Python 3.6.0, pytest-3.0.5, py-1.4.32, pluggy-0.4.0 rootdir: path\to\dir, inifile: pytest.ini collected 2 items test_pytest_ver.py FF ================================== FAILURES =================================== ___ Test_function.test_double_underscore_prefix_function_using_from_import ____ self = <test_pytest_ver.py.Test_function object at 0x04530930> def test_double_underscore_prefix_function_using_from_import(self): > assert __double_underscore_function() == 'double' E NameError: name '_Test_function__double_underscore_function' is not defined test_pytest_ver.py.py:8: NameError ______ Test_function.test_double_undersocre_prefix_function_using_import ______ self = <test_pytest_ver.py.Test_function object at 0x045274D0> def test_double_undersocre_prefix_function_using_import(self): > assert target.__double_underscore_function() == 'double' E AttributeError: module 'target' has no attribute '_Test_function__double_underscore_function' test_pytest_ver.py.py:11: AttributeError ========================== 2 failed in 0.19 seconds ===========================
エラーになり、テストが正常に実行できません。
対応
手元で動かしたところ、以下のどちらかの方法で対応できそうでした(ただ、他にもより良い方法があれば知りたいです)。
- クラスの外側でテストする
- import時に
as
でエイリアスを付ける
クラスを使わないテストコードにする
クラスによるグループ化を諦めて、クラスを使わないテストコードにします。
test_pytest_ver.py
from target import __double_underscore_function def test_double_underscore_prefix_function_using_from_import(): assert __double_underscore_function() == 'double'
実行してみます。
>pytest test_pytest_ver.py ============================= test session starts ============================= platform win32 -- Python 3.6.0, pytest-3.0.5, py-1.4.32, pluggy-0.4.0 rootdir: path\to\dir, inifile: pytest.ini collected 1 items test_pytest_ver.py . ========================== 1 passed in 0.05 seconds ===========================
テストできました。
import時にas
でエイリアスを付ける
クラスでグループ化したい場合は、import ... as
でエイリアスを付けたテストコードにします。
test_pytest_ver.py
from target import __double_underscore_function as double_underscore_function class Test_function(object): def test_double_undersocre_prefix_function_using_from_import_alias(self): assert double_underscore_function() == 'double'
実行してみます。
>pytest test_pytest_ver.py ============================= test session starts ============================= platform win32 -- Python 3.6.0, pytest-3.0.5, py-1.4.32, pluggy-0.4.0 rootdir: path\to\dir, inifile: pytest.ini collected 1 items test_tmp.py . ========================== 1 passed in 0.05 seconds ===========================
テストできました。
その他
プレフィクスがアンダースコア1つの場合
マングリングは働かないため、普通にimportしても動作します。
そのため、
# テスト対象のコード def _single_underscore_function(): return 'single'
というテスト対象コードに対して
# テストコード from target import _single_underscore_function class Test_function(object): def test_single_underscore_prefix_function_using_from_import(self): assert _single_underscore_function() == 'single'
と書けば動作します。
標準モジュールunittestの場合
unittestではunittest.TestCase
を継承してテストコードを書く必要があります。
そのため、import ... as
とする方法しかなさそうです。
import unittest from target import __double_underscore_function import target from target import __double_underscore_function as double_underscore_function class Test_function(unittest.TestCase): @unittest.expectedFailure def test_double_underscore_prefix_function_using_from_import(self): self.assertEqual(__double_underscore_function(), 'double') # => NameError: name '_Test_function__double_function' is not defined @unittest.expectedFailure def test_double_undersocre_prefix_function_using_import(self): assert target.__double_underscore_function() == 'double' # => AttributeError: module 'target' has no attribute '_Test_function__double_underscore_function' def test_double_undersocre_prefix_function_using_from_import_alias(self): self.assertEqual(double_underscore_function(), 'double') # => pass if __name__ == '__main__': unittest.main()
ソースコード
GitHubに上げました。test_double_underscore_prefix_module
ディレクトリ以下が今回のファイルです。
thinkAmi-sandbox/python_pytest-sample