Skip to content

Commit

Permalink
EN-1063 Add Piccolo TestCase subclasses (#1064)
Browse files Browse the repository at this point in the history
* add test cases

* remove accidentally changes

* make sure transaction test gets rolled back

* don't run tests on older Python versions

* update docs

* fix typo

* fix more typos

* one more typo in the docs
  • Loading branch information
dantownsend authored Aug 8, 2024
1 parent 872d58f commit 785a72b
Show file tree
Hide file tree
Showing 30 changed files with 291 additions and 52 deletions.
93 changes: 70 additions & 23 deletions docs/src/piccolo/testing/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <piccolo.table.create_db_tables_sync>` and
:func:`drop_db_tables_sync <piccolo.table.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 <piccolo.table.create_db_tables>` and
:func:`drop_db_tables <piccolo.table.drop_db_tables>` to create and drop the
tables.

.. note::
The async equivalents are :func:`create_db_tables <piccolo.table.create_db_tables>`
and :func:`drop_db_tables <piccolo.table.drop_db_tables>`.
The sync equivalents are :func:`create_db_tables_sync <piccolo.table.create_db_tables_sync>`
and :func:`drop_db_tables_sync <piccolo.table.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 <piccolo.testing.test_case.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
Expand All @@ -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
Expand All @@ -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 <https://github.com/pytest-dev/pytest-asyncio>`_. Simply
Expand All @@ -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 <unittest.IsolatedAsyncioTestCase>`
from Python's standard library. You can then write tests like this:
Expand All @@ -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 <piccolo.testing.test_case.AsyncTransactionTest>`
and :class:`AsyncTableTest <piccolo.testing.test_case.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
120 changes: 120 additions & 0 deletions piccolo/testing/test_case.py
Original file line number Diff line number Diff line change
@@ -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 <piccolo.testing.test_case.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()
2 changes: 1 addition & 1 deletion tests/columns/foreign_key/test_reverse.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
3 changes: 2 additions & 1 deletion tests/columns/test_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
3 changes: 2 additions & 1 deletion tests/columns/test_bigint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests/columns/test_boolean.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests/columns/test_bytea.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
3 changes: 2 additions & 1 deletion tests/columns/test_choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
2 changes: 1 addition & 1 deletion tests/columns/test_date.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests/columns/test_double_precision.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests/columns/test_interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests/columns/test_json.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
3 changes: 2 additions & 1 deletion tests/columns/test_jsonb.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests/columns/test_numeric.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests/columns/test_primary_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests/columns/test_readable.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
Loading

0 comments on commit 785a72b

Please sign in to comment.