diff --git a/.gitignore b/.gitignore index 401fd91..3229383 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -# ignore config +.venv/ + +__pycache__ + +.pytest_cache/ diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..ba1b3cd --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +pytest = "*" + +[packages] +psycopg2-binary = "*" + +[requires] +python_version = "3.7" + +[scripts] diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..f3798e3 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,144 @@ +{ + "_meta": { + "hash": { + "sha256": "2732cd143c656d9b3256de0861c4df2eae10e59286513de09a99222ac596599d" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "psycopg2-binary": { + "hashes": [ + "sha256:040234f8a4a8dfd692662a8308d78f63f31a97e1c42d2480e5e6810c48966a29", + "sha256:086f7e89ec85a6704db51f68f0dcae432eff9300809723a6e8782c41c2f48e03", + "sha256:18ca813fdb17bc1db73fe61b196b05dd1ca2165b884dd5ec5568877cabf9b039", + "sha256:19dc39616850342a2a6db70559af55b22955f86667b5f652f40c0e99253d9881", + "sha256:2166e770cb98f02ed5ee2b0b569d40db26788e0bf2ec3ae1a0d864ea6f1d8309", + "sha256:3a2522b1d9178575acee4adf8fd9f979f9c0449b00b4164bb63c3475ea6528ed", + "sha256:3aa773580f85a28ffdf6f862e59cb5a3cc7ef6885121f2de3fca8d6ada4dbf3b", + "sha256:3b5deaa3ee7180585a296af33e14c9b18c218d148e735c7accf78130765a47e3", + "sha256:407af6d7e46593415f216c7f56ba087a9a42bd6dc2ecb86028760aa45b802bd7", + "sha256:4c3c09fb674401f630626310bcaf6cd6285daf0d5e4c26d6e55ca26a2734e39b", + "sha256:4c6717962247445b4f9e21c962ea61d2e884fc17df5ddf5e35863b016f8a1f03", + "sha256:50446fae5681fc99f87e505d4e77c9407e683ab60c555ec302f9ac9bffa61103", + "sha256:5057669b6a66aa9ca118a2a860159f0ee3acf837eda937bdd2a64f3431361a2d", + "sha256:5dd90c5438b4f935c9d01fcbad3620253da89d19c1f5fca9158646407ed7df35", + "sha256:659c815b5b8e2a55193ede2795c1e2349b8011497310bb936da7d4745652823b", + "sha256:69b13fdf12878b10dc6003acc8d0abf3ad93e79813fd5f3812497c1c9fb9be49", + "sha256:7a1cb80e35e1ccea3e11a48afe65d38744a0e0bde88795cc56a4d05b6e4f9d70", + "sha256:7e6e3c52e6732c219c07bd97fff6c088f8df4dae3b79752ee3a817e6f32e177e", + "sha256:7f42a8490c4fe854325504ce7a6e4796b207960dabb2cbafe3c3959cb00d1d7e", + "sha256:84156313f258eafff716b2961644a4483a9be44a5d43551d554844d15d4d224e", + "sha256:8578d6b8192e4c805e85f187bc530d0f52ba86c39172e61cd51f68fddd648103", + "sha256:890167d5091279a27e2505ff0e1fb273f8c48c41d35c5b92adbf4af80e6b2ed6", + "sha256:9aadff9032e967865f9778485571e93908d27dab21d0fdfdec0ca779bb6f8ad9", + "sha256:9f24f383a298a0c0f9b3113b982e21751a8ecde6615494a3f1470eb4a9d70e9e", + "sha256:a73021b44813b5c84eda4a3af5826dd72356a900bac9bd9dd1f0f81ee1c22c2f", + "sha256:afd96845e12638d2c44d213d4810a08f4dc4a563f9a98204b7428e567014b1cd", + "sha256:b73ddf033d8cd4cc9dfed6324b1ad2a89ba52c410ef6877998422fcb9c23e3a8", + "sha256:dbc5cd56fff1a6152ca59445178652756f4e509f672e49ccdf3d79c1043113a4", + "sha256:eac8a3499754790187bb00574ab980df13e754777d346f85e0ff6df929bcd964", + "sha256:eaed1c65f461a959284649e37b5051224f4db6ebdc84e40b5e65f2986f101a08" + ], + "index": "pypi", + "version": "==2.8.4" + } + }, + "develop": { + "atomicwrites": { + "hashes": [ + "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", + "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" + ], + "version": "==1.3.0" + }, + "attrs": { + "hashes": [ + "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", + "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + ], + "version": "==19.3.0" + }, + "importlib-metadata": { + "hashes": [ + "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", + "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" + ], + "markers": "python_version < '3.8'", + "version": "==0.23" + }, + "more-itertools": { + "hashes": [ + "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", + "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" + ], + "version": "==7.2.0" + }, + "packaging": { + "hashes": [ + "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", + "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108" + ], + "version": "==19.2" + }, + "pluggy": { + "hashes": [ + "sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6", + "sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34" + ], + "version": "==0.13.0" + }, + "py": { + "hashes": [ + "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", + "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" + ], + "version": "==1.8.0" + }, + "pyparsing": { + "hashes": [ + "sha256:4acadc9a2b96c19fe00932a38ca63e601180c39a189a696abce1eaab641447e1", + "sha256:61b5ed888beab19ddccab3478910e2076a6b5a0295dffc43021890e136edf764" + ], + "version": "==2.4.4" + }, + "pytest": { + "hashes": [ + "sha256:27abc3fef618a01bebb1f0d6d303d2816a99aa87a5968ebc32fe971be91eb1e6", + "sha256:58cee9e09242937e136dbb3dab466116ba20d6b7828c7620f23947f37eb4dae4" + ], + "index": "pypi", + "version": "==5.2.2" + }, + "six": { + "hashes": [ + "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", + "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" + ], + "version": "==1.13.0" + }, + "wcwidth": { + "hashes": [ + "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", + "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + ], + "version": "==0.1.7" + }, + "zipp": { + "hashes": [ + "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", + "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" + ], + "version": "==0.6.0" + } + } +} diff --git a/README.md b/README.md index 44287ce..b49d470 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,24 @@ SQL test === +### Prerequisistes + +```bash +$ python3 -m pip install pipenv --user +``` + +- Use virtualenv + +```bash +$ PIPENV_VENV_IN_PROJECT=true pipenv shell +``` + +- Install dependencies + +```bash +$ pipenv install --dev +``` + ### Usage - Run up Postgresql server @@ -26,3 +44,9 @@ $ docker-compose exec db psql -U postgres test ```bash $ docker-compose exec db psql -v ON_ERROR_STOP=1 -U postgres test -a -f "sql/schema.sql" ``` + +- Testing using database + +```bash +$ docker-compose up dbtest +``` diff --git a/docker-compose.yml b/docker-compose.yml index b711319..c1ba88f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,17 @@ version: "3.3" +volumes: + dummy: + services: - db: + pipenv: &pipenv + image: "kennethreitz/pipenv" + working_dir: "/app" + volumes: + - ".:/app" + - "dummy:/app/.venv" + + db: &db container_name: "sql_test_db" image: "mdillon/postgis:10" volumes: @@ -10,3 +20,16 @@ services: - "env/local/.env" ports: - "5438:5432" + + testdb: + <<: *db + container_name: "testdb" + restart: "always" + env_file: + - "env/test/.env" + + dbtest: + <<: *pipenv + command: ["bash", "-c", "pipenv install --dev --deploy --system && pytest -s"] + depends_on: + - testdb diff --git a/env/test/.env b/env/test/.env new file mode 100644 index 0000000..d7718c2 --- /dev/null +++ b/env/test/.env @@ -0,0 +1,6 @@ +POSTGRES_USER=postgres +POSTGRES_PASSWORD=password +POSTGRES_DB=test +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 +# POSTGIS_MAJOR=2.3 diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/dbtest.py b/test/dbtest.py new file mode 100644 index 0000000..d3e8d21 --- /dev/null +++ b/test/dbtest.py @@ -0,0 +1,72 @@ +import unittest + +import functools +import psycopg2 +import os +from typing import Any + + +def dbconnect(func): + @functools.wraps(func) + def inner(*args, **kwargs): + inner.__wrapped__ = func + params = { + "host": "testdb", + "port": 5432, + "dbname": "test", + "user": "postgres", + "password": "password", + } + print(f"Connecting for {params}") + # http://initd.org/psycopg/docs/usage.html#with-statement + conn = None + try: + with psycopg2.connect(**params) as conn: + func(*args, conn=conn, **kwargs) + finally: + if conn: + print(f"Close connection for {params}") + conn.close() + return inner + + +class DbTest(unittest.TestCase): + @dbconnect + def setUp(self, conn): + print("Invoking setUp") + print("Set up database schema") + path_to_schema = os.path.join( + os.path.dirname(__file__), + "..", + "sql", + "schema.sql" + ) + with conn.cursor() as cur: + cur.execute("CREATE SCHEMA IF NOT EXISTS public;") + schema_sql = self.read_file(path_to_schema) + print(f"Loading {path_to_schema}") + cur.execute(schema_sql) + print(f"Loaded {path_to_schema}") + + @dbconnect + def tearDown(self, conn): + print("Invoking tearDown") + print("Tore down database schema") + with conn.cursor() as cur: + print("Droping schema") + cur.execute("DROP SCHEMA IF EXISTS public CASCADE;") + print("Dropped schema") + + + def load_fixtures(self, conn: Any, *path_to_sqls: str) -> None: + for path_to_sql in path_to_sqls: + sql = self.read_file(path_to_sql) + with conn.cursor() as cur: + print(f"Executing {path_to_sql}") + cur.execute(sql) + print(f"Executed {path_to_sql}") + + + def read_file(self, path_to_file: str) -> str: + with open(path_to_file, "r") as f: + return f.read() diff --git a/test/test_example.py b/test/test_example.py new file mode 100644 index 0000000..8e715b8 --- /dev/null +++ b/test/test_example.py @@ -0,0 +1,50 @@ +from .dbtest import ( + DbTest, + dbconnect +) + +import os +from psycopg2.extras import RealDictCursor + + +PATH_TO_SQL_DIR = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", + "sql" + ) +) + +class TestExample(DbTest): + @dbconnect + def test_select_organizations(self, conn): + self.load_fixtures( + conn, + os.path.join(PATH_TO_SQL_DIR, "organizations.sql") + ) + + sql = """ + SELECT * FROM organizations; + """ + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute(sql) + organizations = cur.fetchall() + + assert len(organizations) == 7 + + + @dbconnect + def test_select_addresses(self, conn): + self.load_fixtures( + conn, + os.path.join(PATH_TO_SQL_DIR, "organizations.sql") + ) + + sql = """ + SELECT * FROM addresses; + """ + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute(sql) + addresses = cur.fetchall() + + assert len(addresses) == 7