Skip to content

Getting Started

This guide will walk you to the steps from installation, configuring your fixtures using Elefasts utility functions, to finally using the database in your tests.

Installation

Elefast is available on PyPi, so you should be able to install it using your preferred package manager. Below are some copy-pastable commands for common options.

uv add --dev 'elefast[docker]'
pip install 'elefast[docker]'
poetry add --dev 'elefast[docker]'
pdm add --dev 'elefast[docker]'

Note that they include the docker extra, which you can omit if you don't want to use the Docker-specific functionality.

There are also some other dependencies you most likely want to install:

  • A database driver such as psycopg, psycopg2, or asyncpg
  • pytest since you'll probably want to use it together with this library
  • pytest-asyncio if you intend to use an async driver

Fixture Setup

Once you installed the elefast package, you'll need to create some Pytest fixtures. The following sections will walk you through the recommended set and explain each of them in detail.

The Elefast CLI

To quickly get up and running in a new project, you can use the init command. It ask you some questions about your setup (e.g. which driver you'll use, if you use async or not, etc.) and print boilerplate code to the console.

Make sure you have activated your virtual environment and run it like this:

mkdir tests/ && elefast init > tests/conftest.py

Database Server

Often times Postgres instances only consist of a single database often called postgres. However, Posgres is actually a database server that can house multiple databases at once. We'll use this to our advantage, and use Elefasts utility functions to create a database per test. This isolates our tests and makes sure that data written by test_a does not influence test_b. At the same time, it unlocks running our tests in parallel, e.g. using the wonderful pytest-xdist.

Since you'll probably want to access your database in multiple tests, distributed across multiple files, we'll add them to our tests/conftest.py file.

Info

All code snippets in this guide represent code that will be added to tests/conftest.py. We won't show the whole file contents everytime, just the stuff that you'll need to append if you want to code along.

The first fixture is already the most important one, which sets up our database server.

tests/conftest.py
import pytest
from elefast import DatabaseServer, Database, docker

from my_app.database.models import Base # (1)!

@pytest.fixture(scope="session")
def db_server() -> DatabaseServer:
    db_url = docker.postgres()
    return DatabaseServer(db_url, metadata=Base.metadata)
  1. This is a placeholder. Adjust it to point to your SQLAlchemy base model if you use the ORM.

As you can see, we create a session-scoped fixture. This just means that we'll run this code once during the whole lifetime of a pytest execution / session (Pytest docs).

We obtain a database URL from the postgres method of the docker extra, which automatically starts up a Docker container optimized for testing. If you don't use the docker extra, just pass a sqlalchemy.URL that points enables us to connect to an existing Postgres server.

Finally, we also pass the table metadata object of our ORM base class. This enables Elefast to automatically create all the necessary database tables you need. If you don't use the SQLAlchemy ORM don't worry, this parameter is completely optional and you can just omit it.

Actual Database

Now that we have a fixture that provides us with a database server, we can add another one that creates a database when we need one for a test.

tests/conftest.py
@pytest.fixture
def db(db_server: DatabaseServer): # (1)!
    with db_server.create_database() as database:
        yield database
  1. The argument name here is very important! Since we named our previous fixture function db_server, we now need to give our argument in our db fixture function name and also use db_server! Learn more about this in the "Requesting fixtures" section of the pytest documentation.

As you can see, we don't explicitly pass a fixture scope, which just means that this code will be run for each test that uses this fixture. Next we'll create a database in our Postgres server using a context manager and immediately yield it. This just ensures that we'll delete the database after the test.

Connection Utilities

Now we could already use the db fixture to connect to our database in the tests. However, this would lead to lots of with blocks and indentation, which can be a bit annoying.

So while we are here creating fixtures, let's make our lives a little bit easier and create two more that will help us connect to our database.

tests/conftest.py
@pytest.fixture
def db_connection(db: Database):
    with db.engine.begin() as connection:
        yield connection

@pytest.fixture
def db_session(db: Database):
    with db.session() as session:
        yield session

The first one creates a raw sqlalchemy.Connection object that allows us to run code inside a database transaction. The latter creates an sqlalchemy.orm.Session that we can use to setup specific ORM objects that should exist before we run our test logic, or assert that they exist afterwards.

Info

Feel free to adjust the names to what suits you best. We'll stick to the names from this example throughout the documentation, so you have an easier time cross-referencing code. But if you e.g. mostly interact with your database through the ORM, feel free to rename what we've named db_session to just db, and come up with another name for the fixture we named db (maybe the longer form database?). In the end, this all comes down to personal preference, which is one of the reasons why Elefast does not come with these fixtures out of the box.

Usage In Tests

To then

tests/test_database_math.py
--8<-- "../examples/simple-sync/tests/test_database_math.py"

Now run pytest in your terminal, and you should see our test pass. Behind the scenes, our fixtures have started a Postgres container, maybe created a table structure, and connected to it. However, all of this does not clutter up our actual testing code.

Application Code

Now that we have convenient access to a database in our tests, we probably need to connect our application code. Depending on how your application is architected, there are different ways of having your application code use the testing database.

Explicit Passing

The easiest way is just passing parameters. Imagine we are using the ORM and have a function calculate_statistics.

from sqlalchemy.orm import Session
from my_app.database.models import Post
from my_app.statistics import calculate_statistics

def test_statistics_job(db_session: Session):
    # Arrange
    db_session.add(Post(title="A test", views=100))
    db_session.add(Post(title="Another test", views=100))
    db_session.commit()

    # Act
    statistics = run_calculate_statistics_job(db_session)

    # Assert
    assert statistics.total_views == 200
from dataclasses import dataclass
from sqlalchemy import select, func
from sqlalchemy.orm import Session
from my_app.database.models import Post

@dataclass
class Statistics:
    total_views: int

def calculate_statistics(db: Session): # (1)!
    return Statistics(
        total_views=db.scalar(select(func.count(Post.views)))
    )
  1. By accepting the Session to use as a parameter, we are able to have the test session passed during our test, and an actual one in whatever code uses this function.

As you can see, using our connection to the testing database is trivial. We just pass the connection to the method and everything works as expected.

Environment Variables

A solid solution is to have our application code read from environment variables, as described in "The Twelve-Factor App".

import os
from sqlalchemy import Engine, create_engine

def get_engine() -> Engine:
    db_url = os.getenv("DB_URL")
    if not db_url:
        raise ValueError("We expect a DB_URL environment variable to be present")
    return create_engine(db_url)

and then adjust your db fixture as follows:

@pytest.fixture
def db(db_server: DatabaseServer, monkeypatch: pytest.MonkeyPatch):
    with db_server.create_database() as database:
        db_url = database.url.render_as_string(hide_password=False)
        monkeypatch.setenv("DB_URL", db_url)
        yield database

This will fill the DB_URL environment variable with a proper connection string that is different for each test.

Warning

This will not work if you have a global engine variable laying around in your code. You might have heard that global variables are an anti-pattern, and this highlights one example why that is the case. In this case you can use the monkeypatching approach outlined in the next section. Alternatively you can also store your engine in your application state (our async FastAPI example shows you how), or use something like the get_engine function in the entrypoint of your application and pass it down through function arguments.

Monkeypatching

Another approach is swapping out a global engine variable that might exist in your code. This can look like this

tests/conftest.py
# Add this import
from sqlalchemy import create_engine

@pytest.fixture
def db(db_server: DatabaseServer, monkeypatch: pytest.MonkeyPatch):
    with db_server.create_database() as database:
        engine = create_engine(database.url)
        monkeypatch.setenv("my_app.database", "engine", engine) # (1)!
        yield database
  1. Replace "my_app.database" with the module that contains the global engine variable.

Where To Go From Here

If you've coded along, you have a solid base to run your database-related tests locally. That base can be improved even further, e.g. by allowing If you are interested in making your fixture setup more flexible, head over to the list of recipes. The code snippets over there show you how to connect to your database using async / await, how you , or how to share fixtures in a monorepo.