Skip to content

Commit

Permalink
Use test db for unit tests (#108)
Browse files Browse the repository at this point in the history
* build: Add pytest-postgresql as dev dep

* feat: Include sa engine opts in app config

* tests: WIP add test db / fixtures

* build: Add sqlalchemy-utils to dev deps

* tests: Update test db config/setup

* feat: Clean up db config/setup in tests

* docs: Tweak setup instructions

* docs: Update dev setup + app mgmt instructions

* fix: Try using custom db host in ci
  • Loading branch information
bdewilde authored May 4, 2024
1 parent e2d6c98 commit e6f33b2
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 69 deletions.
1 change: 1 addition & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ on:
env:
COLANDR_DB_USER: "colandr_app"
COLANDR_DB_PASSWORD: "password"
COLANDR_DB_HOST: "localhost"
COLANDR_DB_NAME: "colandr"
COLANDR_DATABASE_URI: "postgresql+psycopg://colandr_app:password@localhost:5432/colandr"
COLANDR_SECRET_KEY: "colandr_secret_key"
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
# `colandr`

Back-end code for [colandr](https://www.colandrapp.com), an ML-assisted online application for conducting systematic reviews and syntheses of text-based evidence.

## local dev setup

Minimal setup instructions, from the beginning, for devs who don't need checks or explanations:

1. Install Xcode: `xcode-select --install`
1. Install Homebrew: `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"`
1. Install Docker and git: `brew cask install docker && brew install git`
1. Clone copy of colandr repo: `git clone https://github.com/datakind/permanent-colandr-back.git`
1. Build and spin up application services: `cd permanent-colandr-back && docker compose up --build --detach`

For more details, see the instructions [here](docs/dev-setup.md).

## app management

(todo: basics here)

For more details, see the instructions [here](docs/app-management.md)
10 changes: 1 addition & 9 deletions colandr/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,7 @@

# sql database config
SQLALCHEMY_DATABASE_URI = os.environ["COLANDR_DATABASE_URI"]
# TODO: figure out how to properly set this setting in a way that works
# from inside and outside docker
# DB_USER = os.environ["COLANDR_DB_USER"]
# DB_PASSWORD = os.environ["COLANDR_DB_PASSWORD"]
# DB_HOST = os.environ["COLANDR_DB_HOST"]
# DB_NAME = os.environ["COLANDR_DB_NAME"]
# SQLALCHEMY_DATABASE_URI = (
# f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:5432/{DB_NAME}"
# )
SQLALCHEMY_ENGINE_OPTIONS = {}
SQLALCHEMY_ECHO = False

# celery+redis config
Expand Down
54 changes: 54 additions & 0 deletions docs/app-management.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,57 @@
# App Management

The colandr back-end Flask application includes a CLI with a few useful commands that may be invoked directly from inside the `colandr-api` container or via docker from outside the container (by prepending `docker exec -it colandr-api`).

Information about all available commands and sub-commands can be had via the `--help` flag:

```shell
$ docker exec -it colandr-api flask --help
```

## Initialize and Migrate the Database

To create the app's database structure -- tables, etc. -- then populate it with data from scratch, run

```shell
$ docker exec -it colandr-api flask db-create
$ docker exec -it colandr-api flask db-seed --fpath /path/to/seed_data.json
```

Technically you can "db-create" whenever you like, but it only creates tables that don't already exist in the database; in contrast, running "db-seed" on an alread-populated database may run into duplicate data violations. To manually _reset_ an existing database by dropping and then re-creating all of its tables, do

```shell
$ docker exec -it colandr-api flask db-reset
```

**Warning:** You will lose all data stored in the database! So be sure to only run this command in development or testing environments.

Database "revisions" are handled through [alembic](https://alembic.sqlalchemy.org/en/latest) using the [`flask-migrate`](https://flask-migrate.readthedocs.io) package. Any time you modify db models -- add a column, remove an index, etc. -- run the following command to generate a migration script:

```shell
$ docker exec -it colandr-api flask db migrate -m [DESCRIPTION]
```

Review and edit the auto-generated file in the `permanent-colandr-back/migrations/versions` directory, since Alembic doesn't necessarily account for every change you can make. When ready, apply the migration to the database:

```shell
$ docker exec -it colandr-api flask db upgrade
```

Lastly, be sure to add and commit the file into version control!


## Unit Testing

Unit tests are implemented and invoked using `pytest`. The test suite is run on the `colandr-api` container, like so:

```shell
$ docker exec -it colandr-api python -m pytest [OPTIONS]
```


---
(old stuff)

## App and DB Management

Most app+db management tasks are handled by the `manage.py` script, found at the top level of the `permanent-colandr-back` directory where the repository was cloned on disk. To get help on available commands, run the following:
Expand Down
52 changes: 6 additions & 46 deletions docs/dev-setup.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
# Dev Setup

Minimal setup instructions, for devs who don't need checks or explanations:

1. Install Xcode: `xcode-select --install`
1. Install Homebrew: `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"`
1. Install Docker: `brew cask install docker`
1. Clone copy of colandr repo: `brew install git && git clone https://github.com/datakind/permanent-colandr-back.git`
1. Build and spin up application services: `docker compose up --build --detach`

As for the rest of us... read on!

These instructions generally assume that you're on a machine running macOS, though most of them should work similarly on Linux. If you've already installed a given tool, there's no need to reinstall -- but you may want to update it.
These instructions generally assume that you're on a machine running macOS, though most if not all of this should work similarly on Linux. If you've already installed a given tool, there's no need to reinstall -- but you may want to update.


## Install System Tools
Expand All @@ -34,23 +24,18 @@ $ brew update
$ brew doctor
```

Lastly, use Homebrew to install [Docker](https://docs.docker.com), a tool for developing and running applications. This should install it in both command line and native application form:
Lastly, use Homebrew to install [Docker](https://docs.docker.com), a tool for developing and running applications, and [git](https://git-scm.com), for version control and access to colandr's code:

```shell
$ brew cask install docker
$ brew install git
```

Confirm that Docker successfully installed by running `docker --version`; for a more extensive check, try `docker run hello-world`. You may also see the Docker icon in your system bar, which may be used to open the Docker for Desktop app.
Confirm that Docker successfully installed by running `docker --version`; for a more extensive check, try `docker run hello-world`. You may also see the Docker icon in your system bar, which can be used to open the Docker for Desktop app.


## Set Up Colandr

Install `git`, for version control and access to the app's code:

```shell
$ brew install git
```

Get a copy of the back-end code from colandr's [GitHub repository](https://github.com/datakind/permanent-colandr-back). Make a new local directory for the repo and change your current working directory to it, as needed:

```shell
Expand All @@ -74,31 +59,6 @@ To build and also run the application stack in "detached" mode (i.e. in the back
$ docker compose up --build --detach
```

The Flask application includes a CLI with a few useful commands that may be invoked directly from inside the `colandr-api` container or via docker from outside the container (by prepending `docker exec -it colandr-api`). To create the app's database structure -- tables, etc. -- then populate it with data from scratch, run

```shell
$ docker exec -it colandr-api flask db-create
$ docker exec -it colandr-api flask db-seed --fpath /path/to/seed_data.json
```

Technically you can "db-create" whenever you like, but it only creates tables that don't already exist in the database; in contrast, running "db-seed" on an alread-populated database may run into duplicate data violations. To manually _reset_ an existing database by dropping and then re-creating all of its tables, do

```shell
$ docker exec -it colandr-api flask db-reset
```

**Note:** You will lose all data stored in the database! So be sure to only run this command in development or testing environments.

Information about all available commands and sub-commands can be had via the `--help` flag:

```shell
$ docker exec -it colandr-api flask --help
```

Unit tests are implemented and invoked using `pytest`. The testing suite may be run on the `colandr-api` container:

```shell
$ docker exec -it colandr-api python -m pytest
```

Interactive API documentation is available in a web browser at "http://localhost:5001/docs". A development email server is available at "http://localhost:8025".

For application management instructions, go [here](./app-management.md)
9 changes: 8 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,14 @@ dependencies = [
]

[project.optional-dependencies]
dev = ["httpx~=0.27.0", "mypy~=1.0", "pytest~=8.0", "ruff~=0.4.0"]
dev = [
"httpx~=0.27.0",
"mypy~=1.0",
"pytest~=8.0",
"pytest-postgresql~=6.0",
"SQLAlchemy-Utils~=0.41.0",
"ruff~=0.4.0",
]

[tool.setuptools.packages.find]
where = ["colandr"]
Expand Down
2 changes: 2 additions & 0 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
httpx~=0.27.0
mypy~=1.0
pytest~=8.0
pytest-postgresql~=6.0
SQLAlchemy-Utils~=0.41.0
ruff~=0.4.0
2 changes: 1 addition & 1 deletion tests/api/test_health.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class TestHealthResource:
def test_get(self, client):
def test_get(self, client, db):
url = "/api/health"
response = client.get(url)
assert response.status_code == 200
Expand Down
50 changes: 38 additions & 12 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,50 @@
import json
import os
import pathlib
import shutil
import typing as t

import flask
import flask_sqlalchemy
import pytest
import sqlalchemy as sa
import sqlalchemy.orm as sa_orm
import sqlalchemy_utils as sa_utils
from pytest_postgresql import factories as psql_factories

from colandr import cli, extensions, models
from colandr.apis import auth
from colandr.app import create_app


# TODO: consider hacking on a solution that doesn't require a running psql db
# for example, this almost but didn't quite work
# import pytest_postgresql.factories
# psql_proc = pytest_postgresql.factories.postgresql_proc(
# host="localhost",
# port=5432,
# user="colandr_app",
# password="PASSWORD",
# dbname="colandr",
# )
# psql_db = pytest_postgresql.factories.postgresql("psql_proc", dbname="colandr")
TEST_DBNAME = "colandr_test"

psql_noproc = psql_factories.postgresql_noproc(
host=os.environ.get("COLANDR_DB_HOST", "colandr-db"),
port=5432,
user=os.environ["COLANDR_DB_USER"],
password=os.environ["COLANDR_DB_PASSWORD"],
dbname=TEST_DBNAME, # override os.environ["COLANDR_DB_NAME"]
)
psql = psql_factories.postgresql("psql_noproc")


@pytest.fixture(scope="session")
def app(tmp_path_factory):
"""Create and configure a new app instance, once per test session."""
config_overrides = {
"TESTING": True,
# override db uri to point at test database
"SQLALCHEMY_DATABASE_URI": (
"postgresql+psycopg://"
f"{os.environ['COLANDR_DB_USER']}:{os.environ['COLANDR_DB_PASSWORD']}"
f"@{os.environ.get('COLANDR_DB_HOST', 'colandr-db')}:5432/{TEST_DBNAME}"
),
# this overrides the app db's default schema (None => "public")
# so that we create a parallel schema for all unit testing data
# "SQLALCHEMY_ENGINE_OPTIONS": {
# "execution_options": {"schema_translate_map": {None: TEST_DB_SCHEMA}}
# },
"SQLALCHEMY_ECHO": True,
"SQLALCHEMY_RECORD_QUERIES": True,
"FULLTEXT_UPLOADS_DIR": str(tmp_path_factory.mktemp("colandr_fulltexts")),
Expand Down Expand Up @@ -62,15 +76,27 @@ def seed_data(seed_data_fpath: pathlib.Path) -> dict[str, t.Any]:
@pytest.fixture(scope="session")
def db(
app: flask.Flask,
psql_noproc,
seed_data_fpath: pathlib.Path,
seed_data: dict[str, t.Any],
request,
):
# create test database if it doesn't already exist
if not sa_utils.database_exists(extensions.db.engine.url):
sa_utils.create_database(extensions.db.engine.url)
# make sure we're starting fresh, tables-wise
extensions.db.drop_all()
extensions.db.create_all()
_store_upload_files(app, seed_data, request)
app.test_cli_runner().invoke(cli.db_seed, ["--fpath", str(seed_data_fpath)])
return extensions.db

yield extensions.db

# NOTE: none of these cleanup commands work :/ it just hangs, and if you cancel it,
# the entire database could get borked owing to a duplicate template database
# so, let's leave test data in place, it's small and causes no harm
# extensions.db.drop_all()
# sa_utils.drop_database(extensions.db.engine.url)


def _store_upload_files(app: flask.Flask, seed_data: dict[str, t.Any], request):
Expand Down

0 comments on commit e6f33b2

Please sign in to comment.