分离应用程序和包测试时的导入问题

Import problem when separating applications and package tests

也许我的目标和我在这里尝试做的事情在 unpythonic 的意义上是错误的。我愿意就此提出任何建议。

我的目标

  1. 具有自己的测试文件夹的应用程序 (myapp)。
  2. 一个包(mypackage)有自己的测试文件夹。
  3. 包的测试应该运行能够从应用程序文件夹和包文件夹中进行。
  4. 该包具有隐式和显式组件。后者需要显式导入(例如通过 import mypackage.mymoduleB)。
  5. 可以将包(文件夹)复制(发送以在其他应用程序中重用?)到其他文件系统位置,而不会失去其功能和可测试性。这就是为什么 tests 在包文件夹内而不是在包文件夹外的原因。

这是文件夹树,其中 itest 是项目名称,myapp 是其中包含 if __name__ == '__main__': 的应用程序,mypackag 是包。

itest
└── myapp
    ├── myapp.py
    ├── mypackage
    │   ├── __init__.py
    │   ├── _mymoduleA.py
    │   ├── mymoduleB.py
    │   └── tests
    │       ├── __init__.py
    │       └── test_all.py
    └── tests
        ├── __init__.py
        └── test_myapp.py

问题

我可以 运行 应用程序目录中的单元测试没有问题。

/home/user/tab-cloud/_transfer/itest/myapp $ python3 -m unittest -vvv
test_A (mypackage.tests.test_all.TestAll) ... mymoduleA.foo()
ok
test_B (mypackage.tests.test_all.TestAll) ... mymoduleB.bar()
ok
test_myname (tests.test_myapp.TestMyApp) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK

但是当我进入包装内部时,测试没有 运行(即目标 #3)。

/home/user/tab-cloud/_transfer/itest/myapp/mypackage $ python3 -m unittest -vvv
tests.test_all (unittest.loader._FailedTest) ... ERROR

======================================================================
ERROR: tests.test_all (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: tests.test_all
Traceback (most recent call last):
  File "/usr/lib/python3.9/unittest/loader.py", line 436, in _find_test_path
    module = self._get_module_from_name(name)
  File "/usr/lib/python3.9/unittest/loader.py", line 377, in _get_module_from_name
    __import__(name)
  File "/home/user/tab-cloud/_transfer/itest/myapp/mypackage/tests/test_all.py", line 12, in <module>
    from . import mypackage
ImportError: cannot import name 'mypackage' from 'tests' (/home/user/tab-cloud/_transfer/itest/myapp/mypackage/tests/__init__.py)


----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (errors=1)

MWE

不,我给你看文件。为了确保在 运行 时使用正确的 import 从应用程序文件夹或我使用的包文件夹 importlib (基于 foreign solution)对包进行测试。

三个文件组成包

这是myapp/mypackage/__init__.py:

# imported implicite via 'mypackage'
from ._mymoduleA import *

# 'mymoduleB' need to be imported explicite
# via 'mypackage.moduleB'

这是myapp/mypackage/_mymoduleA.py:

def foo():
    print('mymoduleA.foo()')
    return 1

这是myapp/mypackage/mymoduleB.py:

def bar():
    print('mymoduleB.bar()')
    return 2

包的测试

myapp/mypackage/tests/__init__.py 为空。

这是myapp/mypackage/tests/test_all.py:

import importlib
import unittest

# The package should be able to be tested by itself (run unittest inside the
# package directory) AND from the using application (run unittest in
# application directory).
# Based on: 
if importlib.util.find_spec('mypackage'):
    import mypackage
    import mypackage.mymoduleB
else:
    from . import mypackage
    from mypackage import mymoduleB


class TestAll(unittest.TestCase):
    def test_A(self):
        self.assertEqual(1, mypackage.foo())

    def test_B(self):
        self.assertEqual(2, mypackage.mymoduleB.bar())

申请

这是cat myapp/myapp.py:

#!/usr/bin/env python3

import mypackage


def myname():
    return 'My application!'


if __name__ == '__main__':
    print(myname())

    mypackage.foo()

    try:
        mypackage.mymoduleB.bar()
    except AttributeError:
        # we expecting this
        print('Not imported yet: "mymoduleB.bar()"')

    # this should work
    import mypackage.mymoduleB
    mypackage.mymoduleB.bar()

应用程序测试

myapp/tests/__init__.py 为空。

这是myapp/tests/test_myapp.py:

import unittest
import myapp


class TestMyApp(unittest.TestCase):
    def test_myname(self):
        self.assertEqual(myapp.myname(), 'My application!')

旁注

请让我解释一下我的目标。 mypackage 应该可以在其他项目中重复使用。实际上,这意味着我将 mypackage 文件夹从一个地方复制到另一个地方。在复制该文件夹时,我确实希望 tests 文件夹随附它而无需明确考虑它,因为它在包文件夹之外。如果新项目进行单元测试,则包的测试应该自动参与该单元测试(通过 discover)。

几年前我创建了一个导入库。它适用于路径。我用它来创建一个插件系统,我基本上可以在其中安装和导入任何库的多个版本(有一些限制)。

为此,我们获取模块的当前路径。然后我们使用路径导入包。该库会自动将正确的路径添加到 sys.path.

您需要做的就是安装 pylibimp pip install pylibimp 并编辑 myapp/mypackage/tests/test_all.py

import os
import pylibimp
import unittest


path_tests = os.path.join(os.path.dirname(__file__))
path_mypackage = os.path.dirname(path_tests)
path_myapp = os.path.dirname(path_mypackage)
mypackage = pylibimp.import_module(os.path.join(path_myapp, 'mypackage'), reset_modules=False)


class TestAll(unittest.TestCase):
    def test_A(self):
        self.assertEqual(1, mypackage.foo())

    def test_B(self):
        self.assertEqual(2, mypackage.mymoduleB.bar())

我觉得背景比较简单

import os
import sys

sys.path.insert(0, os.path.abspath('path/to/myapp'))

# Since path is added we can "import mypackage"
mypackage = __import__('mypackage')

sys.path.pop(0)  # remove the added path to not mess with other imports

希望这就是您要找的。

你的目标真的有点不符合Python。但有的时候,还是要打破常规才能放飞心灵。

您可以通过检查 myapp/mypackage/__init__.py 中的 __package__ 属性来解决问题,如下所示:


# hint from there: 
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

if __package__:
    from ._mymoduleA import foo
else:
    from _mymoduleA import *
    

在这种情况下 myapp/mypackage/tests/test_all.py 代码变得更简单一些:


import importlib
import unittest

if not importlib.util.find_spec('mypackage'):
    from __init__ import *

import mypackage
from mypackage import mymoduleB


class TestAll(unittest.TestCase):
    def test_A(self):
        self.assertEqual(1, mypackage.foo())

    def test_B(self):
        self.assertEqual(2, mymoduleB.bar())

所有其他文件保持不变。

因此,您可以从 /myapp/myapp/mypackage 文件夹中进行 运行 测试。同时,不需要硬编码任何绝对路径。该应用程序可以复制到任何其他文件系统位置。

希望对你有用。