diff --git a/docs/src/piccolo/testing/index.rst b/docs/src/piccolo/testing/index.rst index 39791ab0d..84eabff83 100644 --- a/docs/src/piccolo/testing/index.rst +++ b/docs/src/piccolo/testing/index.rst @@ -100,52 +100,67 @@ Creating the test schema When running your unit tests, you usually start with a blank test database, create the tables, and then install test data. -To create the tables, there are a few different approaches you can take. Here -we use :func:`create_db_tables_sync ` and -:func:`drop_db_tables_sync `. +To create the tables, there are a few different approaches you can take. + +``create_db_tables`` / ``drop_db_tables`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Here we use :func:`create_db_tables ` and +:func:`drop_db_tables ` to create and drop the +tables. .. note:: - The async equivalents are :func:`create_db_tables ` - and :func:`drop_db_tables `. + The sync equivalents are :func:`create_db_tables_sync ` + and :func:`drop_db_tables_sync `, if + you need your tests to be synchronous for some reason. .. code-block:: python - from unittest import TestCase + from unittest import IsolatedAsyncioTestCase - from piccolo.table import create_db_tables_sync, drop_db_tables_sync + from piccolo.table import create_db_tables, drop_db_tables from piccolo.conf.apps import Finder + TABLES = Finder().get_table_classes() - class TestApp(TestCase): - def setUp(self): - create_db_tables_sync(*TABLES) - def tearDown(self): - drop_db_tables_sync(*TABLES) + class TestApp(IsolatedAsyncioTestCase): + async def setUp(self): + await create_db_tables(*TABLES) + + async def tearDown(self): + await drop_db_tables(*TABLES) - def test_app(self): + async def test_app(self): # Do some testing ... pass +You can remove this boiler plate by using +:class:`AsyncTransactionTest `, +which does this for you. + +Run migrations +~~~~~~~~~~~~~~ + Alternatively, you can run the migrations to setup the schema if you prefer: .. code-block:: python - from unittest import TestCase + from unittest import IsolatedAsyncioTestCase from piccolo.apps.migrations.commands.backwards import run_backwards from piccolo.apps.migrations.commands.forwards import run_forwards - from piccolo.utils.sync import run_sync - class TestApp(TestCase): - def setUp(self): - run_sync(run_forwards("all")) - def tearDown(self): - run_sync(run_backwards("all", auto_agree=True)) + class TestApp(IsolatedAsyncioTestCase): + async def setUp(self): + await run_forwards("all") - def test_app(self): + async def tearDown(self): + await run_backwards("all", auto_agree=True) + + async def test_app(self): # Do some testing ... pass @@ -156,7 +171,10 @@ Testing async code There are a few options for testing async code using pytest. -You can either call any async code using Piccolo's ``run_sync`` utility: +``run_sync`` +~~~~~~~~~~~~ + +You can call any async code using Piccolo's ``run_sync`` utility: .. code-block:: python @@ -169,7 +187,10 @@ You can either call any async code using Piccolo's ``run_sync`` utility: rows = run_sync(get_data()) assert len(rows) == 1 -Alternatively, you can make your tests natively async. +It's preferable to make your tests natively async though. + +``pytest-asyncio`` +~~~~~~~~~~~~~~~~~~ If you prefer using pytest's function based tests, then take a look at `pytest-asyncio `_. Simply @@ -182,6 +203,9 @@ like this: rows = await MyTable.select() assert len(rows) == 1 +``IsolatedAsyncioTestCase`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + If you prefer class based tests, and are using Python 3.8 or above, then have a look at :class:`IsolatedAsyncioTestCase ` from Python's standard library. You can then write tests like this: @@ -194,3 +218,26 @@ from Python's standard library. You can then write tests like this: async def test_select(self): rows = await MyTable.select() assert len(rows) == 1 + +Also look at the ``IsolatedAsyncioTestCase`` subclasses which Piccolo provides +(see :class:`AsyncTransactionTest ` +and :class:`AsyncTableTest ` below). + +------------------------------------------------------------------------------- + +``TestCase`` subclasses +----------------------- + +Piccolo ships with some ``unittest.TestCase`` subclasses which remove +boilerplate code from tests. + +.. currentmodule:: piccolo.testing.test_case + +.. autoclass:: AsyncTransactionTest + :class-doc-from: class + +.. autoclass:: AsyncTableTest + :class-doc-from: class + +.. autoclass:: TableTest + :class-doc-from: class diff --git a/piccolo/testing/test_case.py b/piccolo/testing/test_case.py new file mode 100644 index 000000000..085f1e14c --- /dev/null +++ b/piccolo/testing/test_case.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +import typing as t +from unittest import IsolatedAsyncioTestCase, TestCase + +from piccolo.engine import Engine, engine_finder +from piccolo.table import ( + Table, + create_db_tables, + create_db_tables_sync, + drop_db_tables, + drop_db_tables_sync, +) + + +class TableTest(TestCase): + """ + Identical to :class:`AsyncTableTest `, + except it only work for sync tests. Only use this if you can't make your + tests async (perhaps you're on Python 3.7 where ``IsolatedAsyncioTestCase`` + isn't available). + + For example:: + + class TestBand(TableTest): + tables = [Band] + + def test_example(self): + ... + + """ # noqa: E501 + + tables: t.List[t.Type[Table]] + + def setUp(self) -> None: + create_db_tables_sync(*self.tables) + + def tearDown(self) -> None: + drop_db_tables_sync(*self.tables) + + +class AsyncTableTest(IsolatedAsyncioTestCase): + """ + Used for tests where we need to create Piccolo tables - they will + automatically be created and dropped. + + For example:: + + class TestBand(AsyncTableTest): + tables = [Band] + + async def test_example(self): + ... + + """ + + tables: t.List[t.Type[Table]] + + async def asyncSetUp(self) -> None: + await create_db_tables(*self.tables) + + async def asyncTearDown(self) -> None: + await drop_db_tables(*self.tables) + + +class AsyncTransactionTest(IsolatedAsyncioTestCase): + """ + Wraps each test in a transaction, which is automatically rolled back when + the test finishes. + + .. warning:: + Python 3.11 and above only. + + If your test suite just contains ``AsyncTransactionTest`` tests, then you + can setup your database tables once before your test suite runs. Any + changes made to your tables by the tests will be rolled back automatically. + + Here's an example:: + + from piccolo.testing.test_case import AsyncTransactionTest + + + class TestBandEndpoint(AsyncTransactionTest): + + async def test_band_response(self): + \"\"\" + Make sure the endpoint returns a 200. + \"\"\" + band = Band({Band.name: "Pythonistas"}) + await band.save() + + # Using an API testing client, like httpx: + response = await client.get(f"/bands/{band.id}/") + self.assertEqual(response.status_code, 200) + + We add a ``Band`` to the database, but any subsequent tests won't see it, + as the changes are rolled back automatically. + + """ + + # We use `engine_finder` to find the current `Engine`, but you can + # explicity set it here if you prefer: + # + # class MyTest(AsyncTransactionTest): + # db = DB + # + # ... + # + db: t.Optional[Engine] = None + + async def asyncSetUp(self) -> None: + db = self.db or engine_finder() + assert db is not None + self.transaction = db.transaction() + # This is only available in Python 3.11 and above: + await self.enterAsyncContext(cm=self.transaction) # type: ignore + + async def asyncTearDown(self): + await super().asyncTearDown() + await self.transaction.rollback() diff --git a/tests/columns/foreign_key/test_reverse.py b/tests/columns/foreign_key/test_reverse.py index 2a90ac5ba..5bf490c09 100644 --- a/tests/columns/foreign_key/test_reverse.py +++ b/tests/columns/foreign_key/test_reverse.py @@ -1,6 +1,6 @@ from piccolo.columns import ForeignKey, Text, Varchar from piccolo.table import Table -from tests.base import TableTest +from piccolo.testing.test_case import TableTest class Band(Table): diff --git a/tests/columns/test_array.py b/tests/columns/test_array.py index b28204231..085331671 100644 --- a/tests/columns/test_array.py +++ b/tests/columns/test_array.py @@ -14,7 +14,8 @@ ) from piccolo.querystring import QueryString from piccolo.table import Table -from tests.base import TableTest, engines_only, engines_skip, sqlite_only +from piccolo.testing.test_case import TableTest +from tests.base import engines_only, engines_skip, sqlite_only class MyTable(Table): diff --git a/tests/columns/test_bigint.py b/tests/columns/test_bigint.py index 7f418cced..9cb1b8ae4 100644 --- a/tests/columns/test_bigint.py +++ b/tests/columns/test_bigint.py @@ -2,7 +2,8 @@ from piccolo.columns.column_types import BigInt from piccolo.table import Table -from tests.base import TableTest, engines_only +from piccolo.testing.test_case import TableTest +from tests.base import engines_only class MyTable(Table): diff --git a/tests/columns/test_boolean.py b/tests/columns/test_boolean.py index 57268b24a..08f2a504b 100644 --- a/tests/columns/test_boolean.py +++ b/tests/columns/test_boolean.py @@ -2,7 +2,7 @@ from piccolo.columns.column_types import Boolean from piccolo.table import Table -from tests.base import TableTest +from piccolo.testing.test_case import TableTest class MyTable(Table): diff --git a/tests/columns/test_bytea.py b/tests/columns/test_bytea.py index 4b520083f..8114e9325 100644 --- a/tests/columns/test_bytea.py +++ b/tests/columns/test_bytea.py @@ -1,6 +1,6 @@ from piccolo.columns.column_types import Bytea from piccolo.table import Table -from tests.base import TableTest +from piccolo.testing.test_case import TableTest class MyTable(Table): diff --git a/tests/columns/test_choices.py b/tests/columns/test_choices.py index 05502fe0a..d3e1822e5 100644 --- a/tests/columns/test_choices.py +++ b/tests/columns/test_choices.py @@ -2,7 +2,8 @@ from piccolo.columns.column_types import Array, Varchar from piccolo.table import Table -from tests.base import TableTest, engines_only +from piccolo.testing.test_case import TableTest +from tests.base import engines_only from tests.example_apps.music.tables import Shirt diff --git a/tests/columns/test_date.py b/tests/columns/test_date.py index 7cdfb89d9..1628c4758 100644 --- a/tests/columns/test_date.py +++ b/tests/columns/test_date.py @@ -3,7 +3,7 @@ from piccolo.columns.column_types import Date from piccolo.columns.defaults.date import DateNow from piccolo.table import Table -from tests.base import TableTest +from piccolo.testing.test_case import TableTest class MyTable(Table): diff --git a/tests/columns/test_double_precision.py b/tests/columns/test_double_precision.py index bdc8ff387..e29a0e134 100644 --- a/tests/columns/test_double_precision.py +++ b/tests/columns/test_double_precision.py @@ -1,6 +1,6 @@ from piccolo.columns.column_types import DoublePrecision from piccolo.table import Table -from tests.base import TableTest +from piccolo.testing.test_case import TableTest class MyTable(Table): diff --git a/tests/columns/test_interval.py b/tests/columns/test_interval.py index 2e56e6a84..484038003 100644 --- a/tests/columns/test_interval.py +++ b/tests/columns/test_interval.py @@ -3,7 +3,7 @@ from piccolo.columns.column_types import Interval from piccolo.columns.defaults.interval import IntervalCustom from piccolo.table import Table -from tests.base import TableTest +from piccolo.testing.test_case import TableTest class MyTable(Table): diff --git a/tests/columns/test_json.py b/tests/columns/test_json.py index 8b4d8e6cb..19669c61b 100644 --- a/tests/columns/test_json.py +++ b/tests/columns/test_json.py @@ -1,6 +1,6 @@ from piccolo.columns.column_types import JSON from piccolo.table import Table -from tests.base import TableTest +from piccolo.testing.test_case import TableTest class MyTable(Table): diff --git a/tests/columns/test_jsonb.py b/tests/columns/test_jsonb.py index 1e03aa10d..fe90e769b 100644 --- a/tests/columns/test_jsonb.py +++ b/tests/columns/test_jsonb.py @@ -1,6 +1,7 @@ from piccolo.columns.column_types import JSONB, ForeignKey, Varchar from piccolo.table import Table -from tests.base import TableTest, engines_only, engines_skip +from piccolo.testing.test_case import TableTest +from tests.base import engines_only, engines_skip class RecordingStudio(Table): diff --git a/tests/columns/test_numeric.py b/tests/columns/test_numeric.py index cc8c8a604..22c650c70 100644 --- a/tests/columns/test_numeric.py +++ b/tests/columns/test_numeric.py @@ -2,7 +2,7 @@ from piccolo.columns.column_types import Numeric from piccolo.table import Table -from tests.base import TableTest +from piccolo.testing.test_case import TableTest class MyTable(Table): diff --git a/tests/columns/test_primary_key.py b/tests/columns/test_primary_key.py index df041111e..55bff8ee5 100644 --- a/tests/columns/test_primary_key.py +++ b/tests/columns/test_primary_key.py @@ -8,7 +8,7 @@ Varchar, ) from piccolo.table import Table -from tests.base import TableTest +from piccolo.testing.test_case import TableTest class MyTableDefaultPrimaryKey(Table): diff --git a/tests/columns/test_readable.py b/tests/columns/test_readable.py index 3d433b24c..d04036147 100644 --- a/tests/columns/test_readable.py +++ b/tests/columns/test_readable.py @@ -1,7 +1,7 @@ from piccolo import columns from piccolo.columns.readable import Readable from piccolo.table import Table -from tests.base import TableTest +from piccolo.testing.test_case import TableTest class MyTable(Table): diff --git a/tests/columns/test_real.py b/tests/columns/test_real.py index 63ab6a4fe..a2cef5a75 100644 --- a/tests/columns/test_real.py +++ b/tests/columns/test_real.py @@ -1,6 +1,6 @@ from piccolo.columns.column_types import Real from piccolo.table import Table -from tests.base import TableTest +from piccolo.testing.test_case import TableTest class MyTable(Table): diff --git a/tests/columns/test_reference.py b/tests/columns/test_reference.py index 23e3f5ddd..8a10b6207 100644 --- a/tests/columns/test_reference.py +++ b/tests/columns/test_reference.py @@ -8,7 +8,7 @@ from piccolo.columns import ForeignKey, Varchar from piccolo.columns.reference import LazyTableReference from piccolo.table import Table -from tests.base import TableTest +from piccolo.testing.test_case import TableTest class Band(Table): diff --git a/tests/columns/test_reserved_column_names.py b/tests/columns/test_reserved_column_names.py index b87c8b755..1fc4a464e 100644 --- a/tests/columns/test_reserved_column_names.py +++ b/tests/columns/test_reserved_column_names.py @@ -1,6 +1,6 @@ from piccolo.columns.column_types import Integer, Varchar from piccolo.table import Table -from tests.base import TableTest +from piccolo.testing.test_case import TableTest class Concert(Table): diff --git a/tests/columns/test_smallint.py b/tests/columns/test_smallint.py index 75beb4275..808562322 100644 --- a/tests/columns/test_smallint.py +++ b/tests/columns/test_smallint.py @@ -2,8 +2,8 @@ from piccolo.columns.column_types import SmallInt from piccolo.table import Table - -from ..base import TableTest, engines_only +from piccolo.testing.test_case import TableTest +from tests.base import engines_only class MyTable(Table): diff --git a/tests/columns/test_time.py b/tests/columns/test_time.py index f6ab5e7ef..9fc48aaad 100644 --- a/tests/columns/test_time.py +++ b/tests/columns/test_time.py @@ -4,7 +4,8 @@ from piccolo.columns.column_types import Time from piccolo.columns.defaults.time import TimeNow from piccolo.table import Table -from tests.base import TableTest, engines_skip +from piccolo.testing.test_case import TableTest +from tests.base import engines_skip class MyTable(Table): diff --git a/tests/columns/test_timestamp.py b/tests/columns/test_timestamp.py index 0b82c6e42..084da7c6c 100644 --- a/tests/columns/test_timestamp.py +++ b/tests/columns/test_timestamp.py @@ -3,7 +3,7 @@ from piccolo.columns.column_types import Timestamp from piccolo.columns.defaults.timestamp import TimestampNow from piccolo.table import Table -from tests.base import TableTest +from piccolo.testing.test_case import TableTest class MyTable(Table): diff --git a/tests/columns/test_timestamptz.py b/tests/columns/test_timestamptz.py index 62d83a52b..cf3528b9a 100644 --- a/tests/columns/test_timestamptz.py +++ b/tests/columns/test_timestamptz.py @@ -9,7 +9,7 @@ TimestamptzOffset, ) from piccolo.table import Table -from tests.base import TableTest +from piccolo.testing.test_case import TableTest class MyTable(Table): diff --git a/tests/columns/test_uuid.py b/tests/columns/test_uuid.py index 28be64c8a..3dcce88a1 100644 --- a/tests/columns/test_uuid.py +++ b/tests/columns/test_uuid.py @@ -2,7 +2,7 @@ from piccolo.columns.column_types import UUID from piccolo.table import Table -from tests.base import TableTest +from piccolo.testing.test_case import TableTest class MyTable(Table): diff --git a/tests/columns/test_varchar.py b/tests/columns/test_varchar.py index f433cb4c7..c62a3a0fd 100644 --- a/tests/columns/test_varchar.py +++ b/tests/columns/test_varchar.py @@ -1,7 +1,7 @@ from piccolo.columns.column_types import Varchar from piccolo.table import Table - -from ..base import TableTest, engines_only +from piccolo.testing.test_case import TableTest +from tests.base import engines_only class MyTable(Table): diff --git a/tests/query/functions/base.py b/tests/query/functions/base.py index 623bc1a5a..1549709a6 100644 --- a/tests/query/functions/base.py +++ b/tests/query/functions/base.py @@ -1,4 +1,4 @@ -from tests.base import TableTest +from piccolo.testing.test_case import TableTest from tests.example_apps.music.tables import Band, Manager diff --git a/tests/query/functions/test_datetime.py b/tests/query/functions/test_datetime.py index 360833dc4..382f688ab 100644 --- a/tests/query/functions/test_datetime.py +++ b/tests/query/functions/test_datetime.py @@ -12,7 +12,8 @@ Year, ) from piccolo.table import Table -from tests.base import TableTest, engines_only, sqlite_only +from piccolo.testing.test_case import TableTest +from tests.base import engines_only, sqlite_only class Concert(Table): diff --git a/tests/query/functions/test_math.py b/tests/query/functions/test_math.py index 7029e7857..330645c36 100644 --- a/tests/query/functions/test_math.py +++ b/tests/query/functions/test_math.py @@ -3,7 +3,7 @@ from piccolo.columns import Numeric from piccolo.query.functions.math import Abs, Ceil, Floor, Round from piccolo.table import Table -from tests.base import TableTest +from piccolo.testing.test_case import TableTest class Ticket(Table): diff --git a/tests/table/test_refresh.py b/tests/table/test_refresh.py index 2bb9124ec..dd2109c7b 100644 --- a/tests/table/test_refresh.py +++ b/tests/table/test_refresh.py @@ -1,6 +1,7 @@ import typing as t -from tests.base import DBTestCase, TableTest +from piccolo.testing.test_case import TableTest +from tests.base import DBTestCase from tests.example_apps.music.tables import ( Band, Concert, diff --git a/tests/testing/test_test_case.py b/tests/testing/test_test_case.py new file mode 100644 index 000000000..963a3c371 --- /dev/null +++ b/tests/testing/test_test_case.py @@ -0,0 +1,65 @@ +import sys + +import pytest + +from piccolo.engine import engine_finder +from piccolo.testing.test_case import ( + AsyncTableTest, + AsyncTransactionTest, + TableTest, +) +from tests.example_apps.music.tables import Band, Manager + + +class TestTableTest(TableTest): + """ + Make sure the tables are created automatically. + """ + + tables = [Band, Manager] + + async def test_tables_created(self): + self.assertTrue(Band.table_exists().run_sync()) + self.assertTrue(Manager.table_exists().run_sync()) + + +class TestAsyncTableTest(AsyncTableTest): + """ + Make sure the tables are created automatically in async tests. + """ + + tables = [Band, Manager] + + async def test_tables_created(self): + self.assertTrue(await Band.table_exists()) + self.assertTrue(await Manager.table_exists()) + + +@pytest.mark.skipif(sys.version_info <= (3, 11), reason="Python 3.11 required") +class TestAsyncTransaction(AsyncTransactionTest): + """ + Make sure that the test exists within a transaction. + """ + + async def test_transaction_exists(self): + db = engine_finder() + assert db is not None + self.assertTrue(db.transaction_exists()) + + +@pytest.mark.skipif(sys.version_info <= (3, 11), reason="Python 3.11 required") +class TestAsyncTransactionRolledBack(AsyncTransactionTest): + """ + Make sure that the changes get rolled back automatically. + """ + + async def asyncTearDown(self): + await super().asyncTearDown() + + assert Manager.table_exists().run_sync() is False + + async def test_insert_data(self): + await Manager.create_table() + + manager = Manager({Manager.name: "Guido"}) + await manager.save()