Testing

Piccolo provides a few tools to make testing easier.


Test runner

Piccolo ships with a handy command for running your unit tests using pytest. See the tester app.

You can put your test files anywhere you like, but a good place is in a tests folder within your Piccolo app. The test files should be named like test_*. py or *_test.py for pytest to recognise them.


Model Builder

When writing unit tests, it’s usually required to have some data seeded into the database. You can build and save the records manually or use ModelBuilder to generate random records for you.

This way you can randomize the fields you don’t care about and specify important fields explicitly and reduce the amount of manual work required. ModelBuilder currently supports all Piccolo column types and features.

Let’s say we have the following schema:

from piccolo.columns import ForeignKey, Varchar

class Manager(Table):
    name = Varchar(length=50)

class Band(Table):
    name = Varchar(length=50)
    manager = ForeignKey(Manager, null=True)

You can build a random Band which will also build and save a random Manager:

from piccolo.testing.model_builder import ModelBuilder

# Band instance with random values persisted:
band = await ModelBuilder.build(Band)

Note

ModelBuilder.build(Band) persists the record into the database by default.

You can also run it synchronously if you prefer:

manager = ModelBuilder.build_sync(Manager)

To specify any attribute, pass the defaults dictionary to the build method:

manager = ModelBuilder.build(Manager)

# Using table columns:
band = await ModelBuilder.build(
    Band,
    defaults={Band.name: "Guido", Band.manager: manager}
)

# Or using strings as keys:
band = await ModelBuilder.build(
    Band,
    defaults={"name": "Guido", "manager": manager}
)

To build objects without persisting them into the database:

band = await ModelBuilder.build(Band, persist=False)

To build objects with minimal attributes, leaving nullable fields empty:

# Leaves manager empty:
band = await ModelBuilder.build(Band, minimal=True)

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 create_db_tables_sync and drop_db_tables_sync.

Note

The async equivalents are create_db_tables and drop_db_tables.

from unittest import TestCase

from piccolo.table import create_db_tables_sync, drop_db_tables_sync
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)

    def test_app(self):
        # Do some testing ...
        pass

Alternatively, you can run the migrations to setup the schema if you prefer:

from unittest import TestCase

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))

    def test_app(self):
        # Do some testing ...
        pass

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:

from piccolo.utils.sync import run_sync

async def get_data():
    ...

def test_get_data():
    rows = run_sync(get_data())
    assert len(rows) == 1

Alternatively, you can make your tests natively async.

If you prefer using pytest’s function based tests, then take a look at pytest-asyncio. Simply install it using pip install pytest-asyncio, then you can then write tests like this:

async def test_select():
    rows = await MyTable.select()
    assert len(rows) == 1

If you prefer class based tests, and are using Python 3.8 or above, then have a look at IsolatedAsyncioTestCase from Python’s standard library. You can then write tests like this:

from unittest import IsolatedAsyncioTestCase

class MyTest(IsolatedAsyncioTestCase):
    async def test_select(self):
        rows = await MyTable.select()
        assert len(rows) == 1