从 INI 文件获取设置和配置以进行 Pyramid 功能测试

Gettings settings and config from INI file for Pyramid functional testing

在真正的 Pyramid 应用程序中,它根据文档不起作用 http://docs.pylonsproject.org/projects/pyramid//en/latest/narr/testing.html :

class FunctionalTests(unittest.TestCase):
    def setUp(self):
        from myapp import main
        app = main({})

异常:

Traceback (most recent call last):
  File "C:\projects\myapp\tests\model\task_dispatcher_integration_test.py", line 35, in setUp
    app = main({})
  File "C:\projects\myapp\myapp\__init__.py", line 207, in main
    engine = engine_from_config(settings, 'sqlalchemy.')
  File "C:\projects\myapp\ve\lib\site-packages\sqlalchemy\engine\__init__.py", line 407, in engine_from_config
    url = options.pop('url')
KeyError: 'url'

原因很简单:一个空字典被传递给 main,而看起来 运行 真正的应用程序(来自 __init__.py)它得到 settings pre - 填充了 development.ini / production.ini[app:main] 部分的值:

settings {'ldap_port': '4032', 'sqlalchemy.url': 'postgresql://.....}

是否有一些方法可以从 .ini 文件轻松重建 settings 以进行功能测试?

是的,虽然容易是有争议的。

我正在使用以下 py.test 测试夹具来使 --ini 选项传递给测试。然而,这种方法仅限于 py.test 测试运行器,因为其他测试运行器没有这种灵活性。

我的 test.ini 也有特殊设置,例如禁用外发邮件,而是将其打印到终端并测试可访问的积压。

@pytest.fixture(scope='session')
def ini_settings(request):
    """Load INI settings for test run from py.test command line.

    Example:

         py.test yourpackage -s --ini=test.ini


    :return: Adictionary representing the key/value pairs in an ``app`` section within the file represented by ``config_uri``
    """

    if not getattr(request.config.option, "ini", None):
        raise RuntimeError("You need to give --ini test.ini command line option to py.test to find our test settings")

    # Unrelated, but if you need to poke standard Python ConfigParser do it here
    # from websauna.utils.configincluder import monkey_patch_paster_config_parser
    # monkey_patch_paster_config_parser()

    config_uri = os.path.abspath(request.config.option.ini)
    setup_logging(config_uri)
    config = get_appsettings(config_uri)

    # To pass the config filename itself forward 
    config["_ini_file"] = config_uri

    return config

然后我就可以设置app了(注意这里pyramid.paster.bootstrap再次解析配置文件:

@pytest.fixture(scope='session')
def app(request, ini_settings, **settings_overrides):
    """Initialize WSGI application from INI file given on the command line.

    TODO: This can be run only once per testing session, as SQLAlchemy does some stupid shit on import, leaks globals and if you run it again it doesn't work. E.g. trying to manually call ``app()`` twice::

         Class <class 'websauna.referral.models.ReferralProgram'> already has been instrumented declaratively

    :param settings_overrides: Override specific settings for the test case.

    :return: WSGI application instance as created by ``Initializer.make_wsgi_app()``.
    """
    if not getattr(request.config.option, "ini", None):
        raise RuntimeError("You need to give --ini test.ini command line option to py.test to find our test settings")

    data = bootstrap(ini_settings["_ini_file"])
    return data["app"]

进一步设置功能测试服务器:

import threading
import time
from wsgiref.simple_server import make_server
from urllib.parse import urlparse
from pyramid.paster import bootstrap

import pytest
from webtest import TestApp

from backports import typing

#: The URL where WSGI server is run from where Selenium browser loads the pages
HOST_BASE = "http://localhost:8521"


class ServerThread(threading.Thread):
    """ Run WSGI server on a background thread.

    Pass in WSGI app object and serve pages from it for Selenium browser.
    """

    def __init__(self, app, hostbase=HOST_BASE):
        threading.Thread.__init__(self)
        self.app = app
        self.srv = None
        self.daemon = True
        self.hostbase = hostbase

    def run(self):
        """Open WSGI server to listen to HOST_BASE address
        """
        parts = urlparse(self.hostbase)
        domain, port = parts.netloc.split(":")
        self.srv = make_server(domain, int(port), self.app)
        try:
            self.srv.serve_forever()
        except Exception as e:
            # We are a background thread so we have problems to interrupt tests in the case of error
            import traceback
            traceback.print_exc()
            # Failed to start
            self.srv = None

    def quit(self):
        """Stop test webserver."""
        if self.srv:
            self.srv.shutdown()


@pytest.fixture(scope='session')
def web_server(request, app) -> str:
    """py.test fixture to create a WSGI web server for functional tests.

    :param app: py.test fixture for constructing a WSGI application

    :return: localhost URL where the web server is running.
    """

    server = ServerThread(app)
    server.start()

    # Wait randomish time to allows SocketServer to initialize itself.
    # TODO: Replace this with proper event telling the server is up.
    time.sleep(0.1)

    assert server.srv is not None, "Could not start the test web server"

    host_base = HOST_BASE

    def teardown():
        server.quit()

    request.addfinalizer(teardown)
    return host_base

pyramid.paster.get_appsettings 是您唯一需要的东西:

from pyramid.paster import get_appsettings

settings = get_appsettings('test.ini', name='main')
app = main(settings)

test.ini 可以像这样轻松地包含另一个 .ini 文件的所有设置:

[app:main]
use = config:development.ini#main

然后你只需要覆盖那些改变的键(我猜你宁愿针对单独的数据库进行测试):

[app:main]
use = config:development.ini#main
sqlalchemy.uri = postgresql://....

如果其他人没有立即得到@antti-haapala 的回答:

创建一个 test.ini 填充:

[app:main]
use = config:development.ini#main

(实际上这一步不是必需的。您也可以保留 development.ini 并在下面的代码中使用它来代替 test.ini。但是,单独的 test.ini 可能会有用,如果您需要单独的测试设置)

在你的 tests.py 添加:

from pyramid.paster import get_appsettings
settings = get_appsettings('test.ini', name='main')

并替换

app = TestApp(main({}))

app = TestApp(main(global_config = None, **settings))

与此答案相关的是以下评论:

实际上,您不需要导入 get_appsettings,只需添加 像这样的参数:

class FunctionalTests(unittest.TestCase):
def setUp(self):
    from myapp import main
    settings = {'sqlalchemy.url': 'sqlite://'}
    app = main({}, **settings)

这是来源:functional test,它在第二个块代码中,第 31 行。