Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to start a Uvicorn + FastAPI in background when testing with PyTest

I have an REST-API app written with Uvicorn+FastAPI

Which I want to test using PyTest.

I want to start the server in a fixture when I start the tests, so when the test complete, the fixture will kill the app.

FastAPI Testing shows how to test the API app,

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"}

This doesn't bring the server online in the usual way. It seems that the specific functionality that is triggered by the client.get command is the only thing that runs.

I found these additional resources, but I can't make them work for me:

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

How to run server as fixture for py.test

How would you run the Uvicorn+FastAPI app from PyTest, so it goes up and down with the tests?

like image 706
RaamEE Avatar asked Aug 08 '19 12:08

RaamEE


People also ask

How do I use FastAPI on a different port?

One of the options is to use docker-compose and transfer the port in a variable environment. You just need to deploy multiple instances of your application. The version is not a production ready, just a minimal example. 2 applications will start, each on its own port.

Is Pytest better than Unittest?

Which is better – pytest or unittest? Although both the frameworks are great for performing testing in python, pytest is easier to work with. The code in pytest is simple, compact, and efficient. For unittest, we will have to import modules, create a class and define the testing functions within that class.


2 Answers

Inspired from @Gabriel C answer. A fully object oriented and async approach (using the excellent asynctest framework).

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"})
like image 58
Constantin De La Roche Avatar answered Sep 20 '22 14:09

Constantin De La Roche


If you want to bring the server up you will have to do it in a different process/thread, since uvicorn.run() is a blocking call.

Then instead of using the TestClient you will have to use something like requests to hit the actual URL your server is listening to.

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"}
like image 26
Gabriel Cappelli Avatar answered Sep 17 '22 14:09

Gabriel Cappelli