Python + pytestで、プレフィクスがアンダースコア2つの関数(プライベート関数)をテストする

pytestにて、プライベート関数のテストで悩んだことがあったため、メモを残します。

なお、今回のテスト対象コードは以下とします。

target.py

def __double_underscore_function():
    return 'double'

 
目次

 

環境

  • Windows10
  • Python 3.6.0 (32bit)
  • pytest 3.0.5

 

プライベート関数のimportについて

通常のコードの場合

Pythonでは変数・関数・メソッド名の先頭に_ (アンダースコア)があると、それらはプライベートなものとして扱われます。

 
ただ、プライベート関数の場合

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