Skip to content

Test Client: Streamlining Database Testing in Edgy

Have you ever struggled with testing your database interactions, ensuring your model tests target a specific test database instead of your development database? This is a common challenge, often requiring significant setup. Edgy addresses this with its built-in DatabaseTestClient, simplifying your database testing workflow.

Before proceeding, ensure you have the Edgy test client installed with the necessary dependencies:

$ pip install edgy[test]

DatabaseTestClient

The DatabaseTestClient is designed to streamline database testing, automating the creation and management of test databases.

from edgy.testclient import DatabaseTestClient

Parameters

  • url: The database URL, either as a string or a databases.DatabaseURL object.

    from databases import DatabaseURL
    
  • force_rollback: Ensures all database operations are executed within a transaction that rolls back upon disconnection.

    Default: False

  • lazy_setup: Sets up the database on the first connection, rather than during initialization.

    Default: True

  • use_existing: Uses an existing test database if it was previously created and not dropped.

    Default: False

  • drop_database: Drops the test database after the tests have completed.

    Default: False

  • test_prefix: Allows a custom test database prefix. Leave empty to use the URL's database name with a default prefix.

    Default: testclient_default_test_prefix (defaults to test_)

Configuration via Environment Variables

Most default parameters can be overridden using capitalized environment variables prefixed with EDGY_TESTCLIENT_.

For example: EDGY_TESTCLIENT_DEFAULT_PREFIX=foobar or EDGY_TESTCLIENT_FORCE_ROLLBACK=true.

This is particularly useful for configuring tests in CI/CD environments.

Usage

The DatabaseTestClient is designed to be familiar to users of Edgy's Database object, as it extends its functionality with testing-specific features.

Consider a database URL like this:

DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost:5432/my_db"

In this case, the database name is my_db. When using the DatabaseTestClient, it automatically targets a test database named test_my_db.

Here's an example of how to use it in a test:

tests.py
import datetime
import decimal
import ipaddress
import uuid
from datetime import date as local_date
from datetime import datetime as local_datetime
from datetime import time as local_time
from enum import Enum
from typing import Any, Dict
from uuid import UUID

import pytest
from tests.settings import DATABASE_URL

import edgy
from edgy.core.db import fields
from edgy.testclient import DatabaseTestClient

database = DatabaseTestClient(DATABASE_URL, drop_database=True)
models = edgy.Registry(database=database)

pytestmark = pytest.mark.anyio


def time():
    return datetime.datetime.now().time()


class StatusEnum(Enum):
    DRAFT = "Draft"
    RELEASED = "Released"


class Product(edgy.Model):
    # autogenerated for models without primary key
    id: int = fields.IntegerField(primary_key=True, autoincrement=True)
    uuid: UUID = fields.UUIDField(null=True)
    created: local_datetime = fields.DateTimeField(default=datetime.datetime.now)
    created_day: local_date = fields.DateField(default=datetime.date.today)
    created_time: local_time = fields.TimeField(default=time)
    created_date: local_date = fields.DateField(auto_now_add=True)
    created_datetime: local_datetime = fields.DateTimeField(auto_now_add=True)
    updated_datetime: local_datetime = fields.DateTimeField(auto_now=True)
    updated_date: local_date = fields.DateField(auto_now=True)
    data: Dict[str, Any] = fields.JSONField(default={})
    description: str = fields.CharField(default="", max_length=255)
    huge_number: int = fields.BigIntegerField(default=0)
    price: decimal.Decimal = fields.DecimalField(max_digits=5, decimal_places=2, null=True)
    status: Enum = fields.ChoiceField(StatusEnum, default=StatusEnum.DRAFT)
    value: float = fields.FloatField(null=True)

    class Meta:
        registry = models


class User(edgy.Model):
    id: int = fields.UUIDField(primary_key=True, default=uuid.uuid4)
    name: str = fields.CharField(null=True, max_length=16)
    email: str = fields.EmailField(null=True, max_length=256)
    ipaddress: str = fields.IPAddressField(null=True)
    url: str = fields.URLField(null=True, max_length=2048)
    password: str = fields.PasswordField(null=True, max_length=255)

    class Meta:
        registry = models


class Customer(edgy.Model):
    name: str = fields.CharField(null=True, max_length=16)

    class Meta:
        registry = models


@pytest.fixture(autouse=True, scope="module")
async def create_test_database():
    await models.create_all()
    yield
    await models.drop_all()


@pytest.fixture(autouse=True)
async def rollback_transactions():
    with database.force_rollback():
        async with database:
            yield


async def test_model_crud():
    product = await Product.query.create()
    product = await Product.query.get(pk=product.pk)
    assert product.created.year == datetime.datetime.now().year
    assert product.created_day == datetime.date.today()
    assert product.created_date == datetime.date.today()
    assert product.created_datetime.date() == datetime.datetime.now().date()
    assert product.updated_date == datetime.date.today()
    assert product.updated_datetime.date() == datetime.datetime.now().date()
    assert product.data == {}
    assert product.description == ""
    assert product.huge_number == 0
    assert product.price is None
    assert product.status == StatusEnum.DRAFT
    assert product.value is None
    assert product.uuid is None

    await product.update(
        data={"foo": 123},
        value=123.456,
        status=StatusEnum.RELEASED,
        price=decimal.Decimal("999.99"),
        uuid=uuid.UUID("f4e87646-bafa-431e-a0cb-e84f2fcf6b55"),
    )

    product = await Product.query.get()
    assert product.value == 123.456
    assert product.data == {"foo": 123}
    assert product.status == StatusEnum.RELEASED
    assert product.price == decimal.Decimal("999.99")
    assert product.uuid == uuid.UUID("f4e87646-bafa-431e-a0cb-e84f2fcf6b55")

    last_updated_datetime = product.updated_datetime
    last_updated_date = product.updated_date
    user = await User.query.create()
    assert isinstance(user.pk, uuid.UUID)

    user = await User.query.get()
    assert user.email is None
    assert user.ipaddress is None
    assert user.url is None

    await user.update(
        ipaddress="192.168.1.1",
        name="Test",
        email="test@edgy.com",
        url="https://edgy.com",
        password="12345",
    )

    user = await User.query.get()
    assert isinstance(user.ipaddress, (ipaddress.IPv4Address, ipaddress.IPv6Address))
    assert user.password == "12345"

    assert user.url == "https://edgy.com"
    await product.update(data={"foo": 1234})
    assert product.updated_datetime != last_updated_datetime
    assert product.updated_date == last_updated_date

Explanation

This example demonstrates a test using DatabaseTestClient. The client ensures that all database operations within the test are performed on a separate test database, test_my_db in this case.

The drop_database=True parameter ensures that the test database is deleted after the tests have finished running, preventing the accumulation of test databases.

This approach provides a clean and isolated testing environment, ensuring that your tests do not interfere with your development or production databases.