Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FastAPI + Omar + Alembic #5

Open
pawamoy opened this issue Sep 6, 2022 · 6 comments
Open

FastAPI + Omar + Alembic #5

pawamoy opened this issue Sep 6, 2022 · 6 comments

Comments

@pawamoy
Copy link

pawamoy commented Sep 6, 2022

I recently posted this article about testing FastAPI + Omar + Alembic applications: https://pawamoy.github.io/posts/testing-fastapi-ormar-alembic-apps/

The interesting thing in the post is how each test has access to its own, unique, temporary database :)

That's it, just sharing, feel free to close or comment!

@zhanymkanov
Copy link
Owner

Hi @pawamoy,

Thank you for sharing your article, looks very interesting!

I liked the solutions you have proposed, those are very challenging techniques!

From our side, we tried to keep the tests setups as simple as possible, so we used one separate database for all tests, and migrations were run only when needed, yet automatically. The only trick we used was rolling back all the transactions after each test.

@pytest.fixture(autouse=True, scope="session")
def run_migrations():
    import os

    print("running migrations..")
    os.system("alembic upgrade head")

Tests used the same interface for database connection as a main FastAPI app, but with different DATABASE_URL from env and force_rollback=True parameters.

from databases import Database

ROLLBACK_TRANSATIONS = ENVIRONMENT == "testing"
database = Database(DATABASE_URL, force_rollback=ROLLBACK_TRANSACTIONS)

force_rollback=True means every db transaction will be rolled back after disconnection.

@pawamoy
Copy link
Author

pawamoy commented Sep 10, 2022

Hi @zhanymkanov, thanks for your answer and comments! The rollback is very clever, didn't think of that! That will surely be useful to me in other projects/tests 🤔

I'll go ahead and close the issue now 🙂

@pawamoy pawamoy closed this as completed Sep 10, 2022
@zhanymkanov
Copy link
Owner

I will reopen the issue so that people could find this post easily since your article could be helpful for some people writing tests

@zhanymkanov zhanymkanov reopened this Sep 12, 2022
@treharne
Copy link

treharne commented Nov 4, 2022

From @pawamoy

The interesting thing in the post is how each test has access to its own, unique, temporary database :)

From @zhanymkanov

one separate database for all tests .... rolling back all the transactions after each test.

This caught my attention because this year, we moved from the first approach to the second approach and it made our tests 60% faster.

Our old fixture (roughly equivalent to the link @pawamoy sent) was consuming more time than the tests themselves.

@pytest.fixture(autouse=True)
def db_connection() -> App:
    engine = create_engine(DB.url, connect_args=...)
    Base.metadata.create_all(engine)

    yield engine.connect()

    drop_everything(engine)  # carefully deletes tables in the order that they're allowed to be deleted given all FKs

I'm not sure if force_rollback=True is right for us because:

  • some tests connect to the db multiple times
  • I'm not fan of having code to facilitate tests in the main (non-tests) codebase

Here's exactly how we do it now

  1. We decorated our existing db_connection() fixture (above) with @pytest.fixture(scope="session"), so we still have our DB setup, but only once for the whole pytest session.
  2. We have a per-test-function fixture which does the rollback.
@pytest.fixture(autouse=True)
def db_tx_cleanup(db_connection, mocker):
    transaction = db_connection.begin()
    session_factory = sessionmaker(autocommit=False, autoflush=False, bind=db_connection)
    Session = scoped_session(session_factory)

    mocker.patch("backend.db.Session", new=Session)  # This is the *only* place our code can get a DB session.

    yield

    transaction.rollback()

One question @zhanymkanov
Do you mean you have a permanent DB that you run tests against? I have so many questions!!

  • Does each developer have their own local one? (more stuff to set up)
  • Or is it accessed over the network? (then your tests are slower because of network latency)
  • Why did you decide to take this approach? To make it easier to test against Postgres instead of sqlite, and avoid the postgres startup time?

Context
We're using Falcon in our main "monolith", but all our (newer) microservices use FastAPI

@treharne
Copy link

treharne commented Nov 4, 2022

By the way, awesome repo @zhanymkanov . Thank you.

We are evaluating whether it's worth switching from Falcon + Marshmallow to FastAPI + Pydantic. For now I don't think so, but we'll see. This repo really helped me to understand several things I was not sure of for FastAPI in bigger repos.

@zhanymkanov
Copy link
Owner

Hi @treharne,

Thank you for sharing your experience, it's really interesting to read your team's way of doing things!

  1. Yep, that's just a docker compose file for local development. One simple docker compose up -d and you have your local setup :)
  2. Only local.
  3. Yes, I am not a fan of having different databases (postgres + sqlite) for different environments, so we tried to replicate production env as much as possible.
    We have very simple tests to validate the user input and the business logic, so we didn't have to add any extra complexities for tests. Of course, to test against external services we used mocks and fixtures, but for our business logic, the local test database was more than ok.
    It's not perfect, and sometimes clean db didn't show issues we got in production, but that was mostly an anomaly rather than the weakness of tests setups.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants