简体   繁体   English

使用必须导入其他模块的模块运行单元测试

[英]Running unittest with modules that must import other modules

Our Python 3.10 unit tests are breaking when the modules being tested need to import other modules.当正在测试的模块需要导入其他模块时,我们的 Python 3.10 单元测试会中断。 When we use the packaging techniques recommended by other posts and articles, either the unit tests fail to import modules, or the direct calls to run the app fail to import modules.当我们使用其他帖子和文章推荐的打包技术时,单元测试无法导入模块,或者直接调用运行应用程序无法导入模块。 The other posts and articles we have read do not show how to validate that both the application itself and the unit tests can each import modules when called separately.我们阅读的其他帖子和文章没有展示如何验证应用程序本身单元测试在单独调用时是否可以分别导入模块。 So we created a bare bones example below and are asking how to structure the packaging correctly.因此,我们在下面创建了一个简单的示例,并询问如何正确构建包装。

What specific changes must be made to the syntax below in order for the two python commands given below to successfully run on the bare bones example app given below?为了使下面给出的两个 python 命令在下面给出的基本示例应用程序上成功运行,必须对下面的语法进行哪些具体更改?

PROBLEM DESCRIPTION:问题描述:

A python 3.10 app must import modules when called either directly as an app or indirectly through unit tests. python 3.10 应用程序在直接作为应用程序调用或通过单元测试间接调用时必须导入模块。

Packages must be used to organize the code.必须使用包来组织代码。

Calls to unit tests are breaking because modules cannot be found.由于找不到模块,对单元测试的调用正在中断。

The two test commands that must run without errors to validate solution of this problem are:验证此问题的解决方案必须无错误运行的两个测试命令是:

C:\path\to\dir>python repoName\app\first.py

C:\path\to\dir>python -m unittest repoName.unitTests.test_example

This post is different from the other posts on this topic.这篇文章与关于该主题的其他文章不同。 We have reviewed many articles and posts on this topic, but the other sources failed to address our use case, so we have created a more explicit example below to test the two types of commands that must succeed in order to meet the needs of this more explicit use case.我们已经查看了有关该主题的许多文章和帖子,但其他来源未能解决我们的用例,因此我们在下面创建了一个更明确的示例来测试必须成功的两种命令才能满足更多需求显式用例。

APP STRUCTURE:应用程序结构:

The very simple structure of the app that is failing to import packages during unit tests is:在单元测试期间无法导入包的应用程序的非常简单的结构是:

repoName
  app
    __init__.py
    first.py
    second.py
    third.py
  unitTests
    __init__.py
    test_example.py
  __init__.py

SIMPLE CODE TO REPRODUCE PROBLEM:重现问题的简单代码:

The code for a stripped down example to reproduce the problem is as follows:重现问题的精简示例的代码如下:

The contents of repoName\app\__init__.py are: repoName\app\__init__.py的内容是:

print('inside app __init__.py')
__all__ = ['first', 'second', 'third']

The contents of first.py are: first.py的内容是:

import second as second
from third import third
import sys

inputArgs=sys.argv

def runCommands():
  trd = third() 
  if second.something == 'platform':
    if second.another == 'on':
      trd.doThree()
  if second.something != 'unittest' :
    sys.exit(0)

second.processInputArgs(inputArgs)
runCommands()

The contents of second.py are: second.py的内容是:

something = ''
another = ''
inputVars = {}

def processInputArgs(inputArgs):
    global something
    global another
    global inputVars
    if ('unittest' in inputArgs[0]):
      something = 'unittest'
    elif ('unittest' not in inputArgs[0]):
      something = 'platform'
      another = 'on'
    jonesy = 'go'
    inputVars =  { 'jonesy': jonesy }

The contents of third.py are: third.py的内容是:

print('inside third.py')
import second as second

class third:

  def __init__(self):  
    pass

  #@public
  def doThree(self):
    print("jonesy is: ", second.inputVars.get('jonesy'))

The contents of repoName\unitTests\__init__.py are: repoName\unitTests\__init__.py的内容是:

print('inside unit-tests __init__.py')
__all__ = ['test_example']

The contents of test_example.py are: test_example.py的内容是:

import unittest

class test_third(unittest.TestCase):

  def test_doThree(self):
    from repoName.app.third import third
    num3 = third() 
    num3.doThree()
    self.assertTrue(True)

if __name__ == '__main__':
    unittest.main()

The contents of repoName\__init__.py are: repoName\__init__.py的内容是:

print('inside repoName __init__.py')
__all__ = ['app', 'unitTests']

ERROR RESULTING FROM RUNNING COMMANDS:运行命令导致的错误:

The command line response to the two commands are given below.下面给出了对这两个命令的命令行响应。 You can see that the call to the app succeeds, while the call to the unit test fails.可以看到应用调用成功,而单元测试调用失败。

C:\path\to\dir>python repoName\app\first.py
inside third.py
jonesy is:  go

C:\path\to\dir>python -m unittest repoName.unitTests.test_example
inside repoName __init__.py
inside unit-tests __init__.py
inside app __init__.py
inside third.py
E
======================================================================
ERROR: test_doThree (repoName.unitTests.test_example.test_third)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\path\to\dir\repoName\unitTests\test_example.py", line 15, in test_doThree
    from repoName.app.third import third
  File "C:\path\to\dir\repoName\app\third.py", line 3, in <module>
    import second as second
ModuleNotFoundError: No module named 'second'

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (errors=1)

What specific changes must be made to the code above in order for all the modules to be imported correctly when either of the given commands are run?为了在运行任一给定命令时正确导入所有模块,必须对上面的代码进行哪些特定更改?

Creating an "alias" for modules为模块创建“别名”

Update the contents of repoName\app\__init__.py to:repoName\app\__init__.py的内容更新为:

print('inside app __init__.py')
__all__ = ['first', 'second', 'third']

import sys

import repoName.app.second as second
sys.modules['second'] = second

import repoName.app.third as third
sys.modules['third'] = third

import repoName.app.first as first
sys.modules['first'] = first

How to ensure first.py gets run even when imported即使导入,如何确保 first.py 也能运行

So when the test fixture imports repoName.app.third , Python will recursively import the parent packages so that:因此,当测试夹具导入repoName.app.third时, Python 将递归导入父包,以便:

import repoName.app.third is equivalent to import repoName.app.third相当于

import repoName
# inside repoName __init__.py
import app
#inside app __init__.py
import third
#inside third.py

So running from repoName.app.third import third inside test_doThree , executes repoName\app\__init__.py .所以from repoName.app.third import thirdtest_doThree中运行,执行repoName\app\__init__.py In __init__.py , import repoName.app.first as first is called.__init__.py中, import repoName.app.first as first被调用。 Importing first will execute the following lines at the bottom of first.py first导入将在first.py的底部执行以下行

second.processInputArgs(inputArgs)
runCommands()

In second.processInputArgs , jonesy = 'go' is executed setting the variable to be printed out when the rest of the test is ran.second.processInputArgs中,执行jonesy = 'go'设置运行测试的 rest 时要打印的变量。

Here is how I have gone about trying to solve this.以下是我试图解决这个问题的方法。

I exported the PYTHONPATH to the repo folder repoName (I am using linux)我将PYTHONPATH导出到 repo 文件夹repoName (我使用的是 linux)

cd repoName
export PYTHONPATH=`pwd`

then in test_example.py然后在test_example.py

import unittest

class test_third(unittest.TestCase):

  def test_doThree(self):
    from app.third import third # changed here
    num3 = third()
    num3.doThree()
    self.assertTrue(True)

if __name__ == '__main__':
    unittest.main()

Then in third.py然后在third.py

print('inside third.py')
import app.second as second # changed here

class third:

  def __init__(self):  
    pass

  #@public
  def doThree(self):
    print("jonesy is: ", second.inputVars.get('jonesy'))

Also it is worth noting that I did not create any __init__.py files另外值得注意的是,我没有创建任何__init__.py文件

The code in the question relies on first.py being imported so it calls a function in second.py to set a global that is used by third.py .问题中的代码依赖于导入的first.py ,因此它在 second.py 中调用second.py来设置由third.py使用的全局。 As the Zen Of Python says:正如 Python 的禅宗所说:

Explicit is better than implicit显式优于隐式

The current structure will be difficult to maintain, test, and debug as your project grows.随着项目的发展,当前的结构将难以维护、测试和调试。 I have redone the example in the question removing globals and code being executed on import.我已经重做了问题中的示例删除全局变量和导入时执行的代码。

first.py第一个.py

import sys

from app import second
from app.third import Third


def run_commands(input_args):
    trd = Third()
    if input_args.another == "on":
        trd.do_three(input_args)


def main():
    input_args = second.process_input_args(sys.argv)
    run_commands(input_args)


if __name__ == "__main__":
    main()

second.py第二个.py

from dataclasses import dataclass


@dataclass
class InputArgs:
    something: str
    another: str
    jonesy: str


def process_input_args(input_args):
    something = "platform"
    another = "on"
    jonesy = "go"
    return InputArgs(something, another, jonesy)

third.py第三个.py

import sys

print("inside third.py")


class Third:
    def __init__(self):
        pass

    # @public
    def do_three(self, input_args):
        print("jonesy is: ", input_args.jonesy)

test_example.py test_example.py

import io
import unittest
from unittest import mock

from app.second import InputArgs
from app.third import Third


class ThirdTests(unittest.TestCase):
    def test_doThree(self):
        input_args = InputArgs(something="platform",
                               another="on",
                               jonesy="go")

        num3 = Third()
        with unittest.mock.patch('sys.stdout', new=io.StringIO()) as fake_out:
            num3.do_three(input_args)
            self.assertEqual("jonesy is:  go\n", fake_out.getvalue())


if __name__ == "__main__":
    unittest.main()

For Python development I would always recommend having a Python Virtual Environment (venv) so that each repo's development is isolated.对于 Python 开发,我始终建议使用Python 虚拟环境 (venv) ,以便隔离每个 repo 的开发。

In the repoName directory do (for Linux):repoName目录中执行(对于 Linux):

python3.10 -m venv venv

Or like the following for windows:或者像下面的 windows:

c:\>c:\Python310\python -m venv venv

You will then need to activate the venv .然后您需要激活venv

Linux: . venv/bin/activate Linux . venv/bin/activate . venv/bin/activate

Windows: .\venv\scripts\activate.ps1 Windows: .\venv\scripts\activate.ps1

I would suggest packaging the app as your module then all your imports will be of the style:我建议将app打包为您的模块,然后您的所有导入都将采用以下样式:

from app.third import third
trd = third()

or或者

from app import third
trd = third.third()

To package app create a setup.py file in the repoName directory.为 package apprepoName目录中创建一个setup.py文件。 The file will look something like this:该文件将如下所示:

from setuptools import setup

setup(
    name='My App',
    version='1.0.0',
    url='https://github.com/mypackage.git',
    author='Author Name',
    author_email='author@gmail.com',
    description='Description of my package',
    packages=['app'],
    install_requires=[],
    entry_points={
        'console_scripts': ['my-app=app.first:main'],
    },
)

I would also rename the unitTests directory to something like tests so that the unittest module can find it automatically as it looks for files and directories starting with test .我还将unitTests目录重命名为类似tests的目录,以便unittest模块在查找以test开头的文件和目录时可以自动找到它。

So a structure something like this:所以像这样的结构:

repoName/
├── app
│   ├── __init__.py
│   ├── first.py
│   ├── second.py
│   └── third.py
├── setup.py
├── tests
│   ├── __init__.py
│   └── test_example.py
└── venv


You can now do pip install to install from a local src tree in development mode .您现在可以执行pip install在开发模式下从本地 src 树安装。 The great thing about this is that you don't have to mess with the python path or sys.path.这样做的好处是您不必弄乱 python 路径或 sys.path。

(venv) repoName $ pip install -e .
Obtaining file:///home/user/projects/repoName
  Preparing metadata (setup.py) ... done
Installing collected packages: My-App
  Running setup.py develop for My-App
Successfully installed My-App-1.0.0

With the install done the app can be launched:安装完成后,可以启动应用程序:

(venv) repoName $ python app/first.py
inside app __init__.py
inside third.py
jonesy is:  go

In the setup file we told python that my-app was an entry point so we can use this to launch the same thing:在设置文件中,我们告诉 python my-app是一个入口点,因此我们可以使用它来启动相同的东西:

(venv) repoName $ my-app 
inside app __init__.py
inside third.py
jonesy is:  go

For the unittests we can use the following command and it will discover all the tests because we have used test to start directory and file names.对于单元测试,我们可以使用以下命令,它会发现所有测试,因为我们使用test来启动目录和文件名。

(venv) repoName $ python -m unittest 
inside app __init__.py
inside unit-tests __init__.py
inside third.py
jonesy is:  go
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Now we have this setup it is easy to package up app for distribution.现在我们有了这个设置,很容易将 package 启动app进行分发。 Either directly to users or via a Package Index like https://pypi.org/直接发给用户或通过 Package 索引,例如https://pypi.org/

Install the build module:安装构建模块:

(venv) repoName $ pip install --upgrade build

To build the Python wheel:要构建 Python 车轮:

(venv) repoName $ python build

There should now be a dist folder with a wheel in it which you can send to users.现在应该有一个带有轮子的dist文件夹,您可以将其发送给用户。 They can install this with pip:他们可以使用 pip 安装它:

pip install My_App-1.0.0-py3-none-any.whl

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM