Python运行Unittest作为包导入错误

I.前言:应用程序的目录结构和模块在文章末尾列出.

二.问题陈述:如果未设置PYTHONPATH,则应用程序运行,但是单元测试失败,并出现ImportError:没有名为models.transactions的模块.尝试导入时会发生这种情况
app.py中的交易.如果PYTHONPATH设置为/ sandbox / app,则应用程序和
unittest运行没有错误.解决方案的约束条件是不必设置PYTHONPATH,并且
sys.path不必通过编程方式进行修改.

三,详细信息:考虑以下情况:设置PYTHONPATH并将test_app.py作为软件包/ sandbox $python -m unittest tests.test_app运行.查看__main__的打印语句
遍及整个代码:

 models   :  app.models.transactions
 models   :  models.transactions
 resources:  app.resources.transactions
 app      :  app.app
 test     :  tests.test_app

单元测试首先导入应用程序,因此有app.models.transactions.下次导入该应用程序
尝试是resources.transactions.导入时,它会自己导入model.transactions和
然后我们看到__name__代表app.resources.transactions.其次是app.app
导入,最后是unittest模块tests.test.app.设置PYTHONPATH允许应用程序解析模型.

一种解决方案是将model.transactions放入resources.transaction内.但是还有另一种方法可以解决这个问题吗?

为了完整起见,在运行应用程序时,__ main__的打印语句为:

 models   :  models.transactions
 resources:  resources.transactions
 app      :  __main__

这是预期的,并且不会尝试导入高于/ sandbox / app或横向的导入.

IV.附录

A.1目录结构:

|-- sandbox
    |-- app
        |-- models
            |-- __init__.py
            |-- transactions.py 
        |-- resources
            |-- __init__.py
            |-- transactions.py        
        |-- __init__.py
        |-- app.py
    |-- tests
        |-- __init__.py
        |-- test_app.py

A.2模块:

(1)应用程式:

from flask import Flask
from models.transactions import TransactionsModel
from resources.transactions import Transactions
print '     app      : ', __name__
def create_app():
    app = Flask(__name__)
    return app
app = create_app()
if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5000, debug=True)

(2)模型.交易

print '     model    : ', __name__
class TransactionsModel:
    pass

(3)resources.transactions:

from models.transactions import TransactionsModel
print '     resources: ', __name__ 
class Transactions:
    pass

(4)tests.test_app

import unittest 
from app.app import create_app
from app.resources.transactions import Transactions   
print '     test     : ', __name__ 
class DonationTestCase(unittest.TestCase):
    def setUp(self):
        pass
    def tearDown(self):
        pass
    def test_transactions_get_with_none_ids(self):
        self.assertEqual(0, 0) 
if __name__ == '__main__':
    unittest.main()

最佳答案

值得一提的是,Flask文档说要以包的形式运行该应用程序,并设置环境变量:FLASK_APP.然后,该应用程序从项目根目录运行:$python -m flask运行.现在,导入将包括应用程序根目录,例如app.models.transactions.由于单元测试是以相同的方式运行的,因此它作为项目根目录中的包运行,因此所有导入也都在此处解析.

问题的症结可以用以下方式描述. test_app.py需要访问横向导入,但是如果它作为脚本运行,例如:

/sandbox/test$python test_app.py

它具有__name __ == __ main__.这意味着导入(例如来自models.transactions import TransactionsModel)将无法解析,因为它们是横向的,并且在层次结构中并不低.要解决此问题,可以将test_app.py打包运行:

/sandbox$python unittest -m test.test_app

-m开关告诉Python执行此操作.现在,该软件包可以访问app.model,因为它在/ sandbox中运行. test_app.py中的导入必须反映此更改并变为类似以下内容:

from app.models.transactions import TransactionsModel

为了进行测试运行,应用程序中的导入现在必须是相对的.例如,在app.resources中:

from ..models.transactions import TransactionsModel

因此测试成功运行,但是如果应用程序运行,它将失败!这是问题的症结所在.当应用程序从/ sandbox / app $python app.py作为脚本运行时,它将命中此相对导入..models.transactions,并返回错误,表示该程序正尝试在最高级别上导入.解决一个,然后打破另一个.

无需设置PYTHONPATH,如何解决此问题?可能的解决方案是在包__init__.py中使用条件进行条件导入.资源包的示例如下:

if __name__ == 'resources':
    from models.transactions import TransactionsModel
    from controllers.transactions import get_transactions
elif __name__ == 'app.resources':
    from ..models.transactions import TransactionsModel
    from ..controllers.transactions import get_transactions

要克服的最后一个障碍是如何将其拉入resources.py.在__init__.py中完成的导入已绑定到该文件,不能用于resources.py.通常,其中将在resource.py中包括以下导入:

import resources

但是,还是资源还是app.resources?看来困难对我们来说已经走得更远了. importlib提供的工具可以在这里提供帮助,例如,以下将进行正确的导入:

from importlib import import_module
import_module(__name__)

还有其他可以使用的方法.例如,

TransactionsModel = getattr(import_module(__name__), 'TransactionsModel')

这解决了当前情况下的错误.

另一个更直接的解决方案是在模块本身中使用绝对导入.例如,在资源中:

models_root = os.path.join(os.path.dirname(__file__), '..', 'models')
fp, file_path, desc = imp.find_module(module_name, [models_root])
TransactionsModel = imp.load_module(module_name, fp, file_path, 
    desc).TransactionsModel
TransactionType = imp.load_module(module_name, fp, file_path, 
    desc).TransactionType

只是有关使用sys.path.append(app_root)更改PYTHONPATH的说明
在resources.py中.这很好用,并且只需几行代码即可.此外,它仅更改执行文件的路径,并在完成后还原.似乎是单元测试的好用例.一个问题可能是当应用程序移至不同的环境时.