【问题标题】:How to start a Uvicorn + FastAPI in background when testing with PyTest使用 PyTest 进行测试时如何在后台启动 Uvicorn + FastAPI
【发布时间】:2020-10-21 08:03:43
【问题描述】:

我有一个用 Uvicorn+FastAPI 编写的 REST-API 应用程序

我想使用 PyTest 进行测试。

我想在开始测试时在夹具中启动服务器,所以当测试完成时,夹具将终止应用程序。

FastAPI Testing 展示了如何测试 API 应用程序,

from fastapi import FastAPI
from starlette.testclient import TestClient

app = FastAPI()


@app.get("/")
async def read_main():
    return {"msg": "Hello World"}


client = TestClient(app)


def test_read_main():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"msg": "Hello World"}

这不会以通常的方式使服务器联机。似乎由 client.get 命令触发的特定功能是唯一运行的东西。

我找到了这些额外的资源,但我无法让它们为我工作:

https://medium.com/@hmajid2301/pytest-with-background-thread-fixtures-f0dc34ee3c46

How to run server as fixture for py.test

您将如何从 PyTest 运行 Uvicorn+FastAPI 应用程序,以便它随着测试上升和下降?

【问题讨论】:

标签: python testing pytest fastapi uvicorn


【解决方案1】:

灵感来自@Gabriel C 的回答。完全面向对象的异步方法(使用出色的异步测试框架)。

import logging
from fastapi import FastAPI

class App:
    """ Core application to test. """

    def __init__(self):
        self.api = FastAPI()
        # register endpoints
        self.api.get("/")(self.read_root)
        self.api.on_event("shutdown")(self.close)

    async def close(self):
        """ Gracefull shutdown. """
        logging.warning("Shutting down the app.")

    async def read_root(self):
        """ Read the root. """
        return {"Hello": "World"}

""" Testing part."""
from multiprocessing import Process
import asynctest
import asyncio
import aiohttp
import uvicorn

class TestApp(asynctest.TestCase):
    """ Test the app class. """

    async def setUp(self):
        """ Bring server up. """
        app = App()
        self.proc = Process(target=uvicorn.run,
                            args=(app.api,),
                            kwargs={
                                "host": "127.0.0.1",
                                "port": 5000,
                                "log_level": "info"},
                            daemon=True)
        self.proc.start()
        await asyncio.sleep(0.1)  # time for the server to start

    async def tearDown(self):
        """ Shutdown the app. """
        self.proc.terminate()

    async def test_read_root(self):
        """ Fetch an endpoint from the app. """
        async with aiohttp.ClientSession() as session:
            async with session.get("http://127.0.0.1:5000/") as resp:
                data = await resp.json()
        self.assertEqual(data, {"Hello": "World"})

【讨论】:

  • asynctest 为单元测试带来了哪些附加价值?我可以理解它对于端到端测试或负载测试等可能很重要,但对于单元测试,没有得到它。
  • asynctest 是 unittest 之上的测试框架,方便测试协程。 Unittest 只能测试同步功能,但可能从那以后它发生了变化。
  • 我认为您不需要异步测试套件来测试异步 FastAPI 功能。这是教程:fastapi.tiangolo.com/tutorial/testing.
  • Here 我有另一个解决方案,可以在同一进程中启动服务器并正常关闭。
  • 从我的新手的角度来看,如果你引入了 OP 不需要的新元素,比如异步方法,那么解释为什么需要这样做或者如果不需要它有什么好处,将是赞赏。这也将帮助那些在没有这种方法的情况下无法使其工作的人找出它是否是必需的以及为什么。
【解决方案2】:

如果你想启动服务器,你必须在不同的进程/线程中进行,因为 uvicorn.run() 是一个阻塞调用。

然后,您将不得不使用诸如请求之类的东西来访问您的服务器正在侦听的实际 URL,而不是使用 TestClient。

from multiprocessing import Process

import pytest
import requests
import uvicorn
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def read_main():
    return {"msg": "Hello World"}


def run_server():
    uvicorn.run(app)


@pytest.fixture
def server():
    proc = Process(target=run_server, args=(), daemon=True)
    proc.start() 
    yield
    proc.kill() # Cleanup after test


def test_read_main(server):
    response = requests.get("http://localhost:8000/")
    assert response.status_code == 200
    assert response.json() == {"msg": "Hello World"}

【讨论】:

  • 这不适用于 pytest >= 4.0,因为它不再支持 yield
  • 我刚刚用 pytest 4.0.0 和 5.4.2 进行了测试,yield 仍然有效。在documentation 它甚至说你应该使用这种方法
  • 它在documentation 中说,yield-test 已被弃用。就我而言,我无法让它以产量运行。没有它,服务器不会停止
  • 根据您的输入,我使用fixture(scope="module")yield proc 运行它(而不仅仅是yield)。非常感谢!
  • @M.Winkens,您说的是 测试函数 中的 yields,它已被弃用。在这个例子中,yield 在fixture 中,完全没有被弃用。给你:docs.pytest.org/en/2.8.7/yieldfixture.html#yieldfixture
【解决方案3】:

这里我有另一个在同一进程中运行 uvicorn 的解决方案(使用 Python 3.7.9 测试):

from typing import List, Optional
import asyncio

import pytest

import uvicorn

PORT = 8000


class UvicornTestServer(uvicorn.Server):
    """Uvicorn test server

    Usage:
        @pytest.fixture
        server = UvicornTestServer()
        await server.up()
        yield
        await server.down()
    """

    def __init__(self, app, host='127.0.0.1', port=PORT):
        """Create a Uvicorn test server

        Args:
            app (FastAPI, optional): the FastAPI app. Defaults to main.app.
            host (str, optional): the host ip. Defaults to '127.0.0.1'.
            port (int, optional): the port. Defaults to PORT.
        """
        self._startup_done = asyncio.Event()
        super().__init__(config=uvicorn.Config(app, host=host, port=port))

    async def startup(self, sockets: Optional[List] = None) -> None:
        """Override uvicorn startup"""
        await super().startup(sockets=sockets)
        self.config.setup_event_loop()
        self._startup_done.set()

    async def up(self) -> None:
        """Start up server asynchronously"""
        self._serve_task = asyncio.create_task(self.serve())
        await self._startup_done.wait()

    async def down(self) -> None:
        """Shut down server asynchronously"""
        self.should_exit = True
        await self._serve_task


@pytest.fixture
async def startup_and_shutdown_server():
    """Start server as test fixture and tear down after test"""
    server = UvicornTestServer()
    await server.up()
    yield
    await server.down()


@pytest.mark.asyncio
async def test_chat_simple(startup_and_shutdown_server):
    """A simple websocket test"""
    # any test code here

【讨论】:

  • 对我不起作用。服务器只是卡住了,没有回复。试图在浏览器中联系主机:同样的故事。必须用 kill -9 杀死它。 Python 3.7.5
  • @GlaIZier,您的测试代码也必须是异步的。你一直在使用请求吗?你应该使用 aiohttp。如果你想让服务器响应,一切都必须是非阻塞的。
猜你喜欢
  • 1970-01-01
  • 2021-09-11
  • 2021-05-21
  • 1970-01-01
  • 2021-08-31
  • 2022-10-05
  • 2020-12-01
  • 2022-01-23
  • 1970-01-01
相关资源
最近更新 更多