Шаблон фабрики приложений WSGI и время импорта созданных объектов уровня модуля - PullRequest
0 голосов
/ 24 апреля 2018

Я некоторое время думал о фабричном шаблоне для приложений WSGI, в соответствии с рекомендациями Flask docs . В частности, о тех функциях, которые обычно показаны для использования объектов, которые были созданы во время импорта модуля, как, например, db в отличие от того, что были созданы в фабричной функции.

Будет ли фабричная функция идеально создавать _everything_ заново или не имеет смысла для таких объектов, как db engine? (Я думаю, здесь более чистое разделение и лучшая тестируемость.)

Вот некоторый код, в котором я пытаюсь завершить создание всех необходимых объектов для приложения wsgi. в заводской функции.

# factories.py
def create_app(config, engine=None):
    """Create WSGI application to be called by WSGI server. Full factory function
    that takes care to deliver entirely new WSGI application instance with all
    new member objects like database engine etc.

    Args:
        config (dict): Dict to update the wsgi app. configuration.
        engine (SQLAlchemy engine): Database engine to use.
    """

    # flask app
    app = Flask(__name__)  # should be package name instead of __name__ acc. to docs
    app.config.update(config)

    # create blueprint
    blueprint = ViewRegistrationBlueprint('blueprint', __name__, )
    # request teardown behaviour, always called, even on unhandled exceptions

    # register views for blueprint
    from myapp.views import hello_world
    # dynamically scrapes module and registers methods as views
    blueprint.register_routes(hello_world)

    # create engine and request scoped session for current configuration and store
    # on wsgi app
    if (engine is not None):

        # delivers transactional scope when called
        RequestScopedSession = scoped_session(
            sessionmaker(bind=engine),
            scopefunc=flask_request_scope_func
        )

        def request_scoped_session_teardown(*args, **kwargs):
            """Function to register and call by the framework when a request is finished
            and the session should be removed.
            """
            # wrapped in try/finally to make sure no error collapses call stack here
            try:
                RequestScopedSession.remove()  # rollback all pending changes, close and return conn. to pool
            except Exception as exception_instance:
                msg = "Error removing session in request teardown.\n{}"
                msg = msg.format(exception_instance)
                logger.error(msg)
            finally:
                pass

        app.config["session"] = RequestScopedSession
        blueprint.teardown_request(request_scoped_session_teardown)

    # register blueprint
    app.register_blueprint(blueprint)

    return app


def create_engine(config):
    """Create database engine from configuration

    Args:
        config (dict): Dict used to assemble the connection string.
    """

    # connection_string
    connection_string = "{connector}://{user}:{password}@{host}/{schema}"
    connection_string = connection_string.format(**config)

    # database engine
    return sqlalchemy_create_engine(
        connection_string,
        pool_size=10,
        pool_recycle=7200,
        max_overflow=0,
        echo=True
    )
# wsgi.py (served by WSGI server)
from myapp.factories import create_app
from myapp.factories import create_engine
from myapp.configuration.config import Config

config = Config()

engine = create_engine(config.database_config)
app = create_app(config.application_config, engine=engine)
# conftest.py
from myapp.factories import create_app
from myapp.factories import create_engine
from myapp.configuration.config import Config

@pytest.fixture
def app():
    config = TestConfig()
    engine = create_engine(config.database_config)
    app = create_app(config.application_config, engine=engine)
    with app.app_context():
        yield app

1 Ответ

0 голосов
/ 30 апреля 2018

Поскольку вы также пометили это как sanic Я отвечу с этим фоном.Sanic является асинхронным и, таким образом, опирается на цикл обработки событий.Цикл событий - это ресурс, и поэтому его нельзя разделять между тестами, а создавать заново для каждого.Следовательно, соединение с базой данных и т. Д. Также необходимо создавать для каждого теста, и его нельзя использовать повторно, поскольку оно асинхронно и зависит от цикла событий.Даже без асинхронной природы было бы самым чистым создавать подключения БД для каждого теста, потому что они имеют состояние (например, временные таблицы).

Так что я получил create_app(), который создает все, что позволяет мне создавать произвольныеколичество независимых приложений в тестовом прогоне.(Честно говоря, есть некоторые глобальные ресурсы, такие как зарегистрированные слушатели событий, но их легко разбить на фабриках py.test.) Для тестируемости я бы попытался избежать глобальных ресурсов, которые создаются при импорте модулей.Хотя я видел по-другому в больших и успешных проектах.

Это не совсем определенный ответ, я знаю ...

...