在不启动应用程序服务器的情况下对 Sanic 中的侦听器进行单元测试
Unit test a listener in Sanic without starting the app server
假设我在我的 Sanic 应用程序中定义了这个侦听器:
@app.before_server_start
async def db_setup(*args):
# ... set up the DB as I wish for the app
如果我想对这个函数进行单元测试(使用 pytest)并使用 from my.app import db_setup
将其导入到单元测试文件中,似乎测试实际上开始为应用程序提供服务,因为 pytest 输出:
[INFO] Goin' Fast @ http://0.0.0.0:8000
[INFO] Starting worker [485]
现在,我知道我可以通过 db_setup = db_setup.__wrapped__
删除装饰器的效果,但为了做到这一点,我实际上需要导入 db_setup
,这是 Sanic 服务器触发的地方向上。
有没有办法在导入时删除装饰器的效果?
LE:我试过如下修补 Sanic 应用程序:
async def test_stuff(mocker):
mocker.patch('myapp.app.app') # last '.app' being `app = Sanic('MyApp')`
imp = importlib.import_module('myapp.app')
db_setup = getattr(imp, 'db_setup')
await db_setup()
但现在我得到了 mocker.patch('myapp.app.app')
行的 RuntimeError: Cannot run the event loop while another loop is running
。
我将在这里做一些假设,所以如果有一些澄清,我可能需要修改这个答案。
在开始之前,应该注意装饰器本身不会启动您的网络服务器。这将 运行 在以下两种情况之一:
- 您正在运行宁
app.run()
全局范围内的某个地方
- 您正在使用 Sanic
TestClient
,它专门通过 运行 连接您的应用程序的 Web 服务器 来运行
现在,据我所知,您正试图通过将其作为函数调用来手动 运行 db_setup
测试,但您 没有 希望它作为侦听器附加到您的测试中的应用程序。
您可以在 app.listeners
属性 中访问所有应用程序实例的侦听器。因此,一种解决方案是这样的:
# conftest.py
import pytest
from some.place import app as myapp
@pytest.fixture
def app():
myapp.listeners = {}
return myapp
正如我之前所说,这只会清空您的听众。它实际上不会影响您的应用程序启动,所以我不确定它是否具有您正在寻找的实用程序。
你应该可以拥有这样的东西:
from unittest.mock import Mock
import pytest
from sanic import Request, Sanic, json
app = Sanic(__name__)
@app.get("/")
async def handler(request: Request):
return json({"foo": "bar"})
@app.before_server_start
async def db_setup(app, _):
app.ctx.db = 999
@pytest.mark.asyncio
async def test_sample():
await db_setup(app, Mock())
assert app.ctx.db == 999
为了方便,都在同一个范围内,但即使测试函数、应用实例、监听器分布在不同的模块中,最终的结果也是一样的:你可以运行 db_setup
与任何其他函数一样,是否注册为侦听器并不重要。
假设我在我的 Sanic 应用程序中定义了这个侦听器:
@app.before_server_start
async def db_setup(*args):
# ... set up the DB as I wish for the app
如果我想对这个函数进行单元测试(使用 pytest)并使用 from my.app import db_setup
将其导入到单元测试文件中,似乎测试实际上开始为应用程序提供服务,因为 pytest 输出:
[INFO] Goin' Fast @ http://0.0.0.0:8000
[INFO] Starting worker [485]
现在,我知道我可以通过 db_setup = db_setup.__wrapped__
删除装饰器的效果,但为了做到这一点,我实际上需要导入 db_setup
,这是 Sanic 服务器触发的地方向上。
有没有办法在导入时删除装饰器的效果?
LE:我试过如下修补 Sanic 应用程序:
async def test_stuff(mocker):
mocker.patch('myapp.app.app') # last '.app' being `app = Sanic('MyApp')`
imp = importlib.import_module('myapp.app')
db_setup = getattr(imp, 'db_setup')
await db_setup()
但现在我得到了 mocker.patch('myapp.app.app')
行的 RuntimeError: Cannot run the event loop while another loop is running
。
我将在这里做一些假设,所以如果有一些澄清,我可能需要修改这个答案。
在开始之前,应该注意装饰器本身不会启动您的网络服务器。这将 运行 在以下两种情况之一:
- 您正在运行宁
app.run()
全局范围内的某个地方 - 您正在使用 Sanic
TestClient
,它专门通过 运行 连接您的应用程序的 Web 服务器 来运行
现在,据我所知,您正试图通过将其作为函数调用来手动 运行 db_setup
测试,但您 没有 希望它作为侦听器附加到您的测试中的应用程序。
您可以在 app.listeners
属性 中访问所有应用程序实例的侦听器。因此,一种解决方案是这样的:
# conftest.py
import pytest
from some.place import app as myapp
@pytest.fixture
def app():
myapp.listeners = {}
return myapp
正如我之前所说,这只会清空您的听众。它实际上不会影响您的应用程序启动,所以我不确定它是否具有您正在寻找的实用程序。
你应该可以拥有这样的东西:
from unittest.mock import Mock
import pytest
from sanic import Request, Sanic, json
app = Sanic(__name__)
@app.get("/")
async def handler(request: Request):
return json({"foo": "bar"})
@app.before_server_start
async def db_setup(app, _):
app.ctx.db = 999
@pytest.mark.asyncio
async def test_sample():
await db_setup(app, Mock())
assert app.ctx.db == 999
为了方便,都在同一个范围内,但即使测试函数、应用实例、监听器分布在不同的模块中,最终的结果也是一样的:你可以运行 db_setup
与任何其他函数一样,是否注册为侦听器并不重要。