Backend Development

Poetry

We use poetry for dependency management and creating virtual environments. To add requirements to minor-illusion, use poetry add <package name>. If it's a development dependency, such as for testing, include the --dev flag.

To set up a development environment on your localhost, navigate to the backend/src directory and run poetry install. That should create a .venv directory and install all dependencies there. Afterwards, you can activate that virtual environment with poetry shell or use poetry run, e.g. poetry run pytest.

The same poetry install and .venv directory creation happens during the backend Docker build. Alembic migrations and the Hypercorn server are both executed with poetry run as a prefix.

Unit Testing

Unit tests are written with pytest in the backend/src/tests directory. They can be run on your localhost directly with poetry (poetry install; poetry run pytest), or using tox. Alternatively, you can spin up the backend container and run tests in there (docker-compose run backend tox). Lastly, the tox tests are run in Github CI/CD during any PR to the main branch.

Database testing

There are several ways to test database interaction in a Python application. Some common approaches are:

  • Mock out the database entirely
  • Use an in-memory sqlite3 database as a stand-in for production
  • Maintain a persistent test database alongside the production database

We are able to effectively mock out the database interactions by using a data access object (DAO) strategy, in which all SQL statement execution is encapsulated in @classmethod's defined in the SQLAlchemy models. In tests, we can mock those DAO classes with FakeDAO classes, overriding the appropriate @classmethod behavior to return fake data. This is a good way to test database interactions without actually connecting to a real database.

Additionally, CockroachDB offers an ephemeral in-memory deployment option with the cockroach demo command, including enterprise license support for limited time. In this strategy, a pytest fixture begins running the in-memory cockroach database, creates the tables, and patches the db_session to have a connection to the test database instead of production.

Integration Testing

We use Jupyter Notebooks as one way of testing expected behavior in running applications without any of the mocking that happens in unit tests. The Jupyter container that gets launched with this repo has the backend code base volume mounted into it, and can also make HTTP calls to the backend application if it's up. There are Notebooks in jupyter/notebooks demonstrating how to pull from the Cockroach DB directly using the backend's SQLAlchemy models as well as "integration test" style Notebooks that hit the REST API.

Database Migrations

We use Alembic for database migrations. The typical development pattern is to edit SQLAlchemy models (backend/src/app/models.py) and then auto-generate new revisions. It's worth reviewing the gotchas that come along with --autogenerate.

If you want to see how this works in practice, the easiest thing to do is delete everything out of backend/src/migrations/versions. Then bring up the minor-illusion app (docker-compose up --build -d) and you should observe that there is no table creation or seed data creation by the backend app. Exec into the backend container (docker-compose exec backend /bin/bash), activate the virtual environment with poetry shell, and run alembic revision --autogenerate.

Alembic knows about the models in the backend app thanks to a line in backend/src/migrations/env.py, target_metadata = BaseDAO.metadata It will compare what those introspected models should look like with what schemas are in Cockroach DB. Then it will create a revision file in backend/src/migrations/versions. When you run alembic upgrade head after that (still in the backend container) then you'll see alembic applying appropriate SQL commands to bring Cockroach DB up to date with the table schemas defined in your models.py. alembic downgrade base will roll back all migrations.